Merge branch 'develop' of https://codeberg.org/calckey/calckey into note-improvements
This commit is contained in:
commit
9d1c08d322
|
@ -66,7 +66,7 @@ If you have access to a server that supports one of the sources below, I recomme
|
||||||
|
|
||||||
### 🐋 Docker
|
### 🐋 Docker
|
||||||
|
|
||||||
[How to run Calckey with Docker](./docker-README.md).
|
[How to run Calckey with Docker](./docs/docker.md).
|
||||||
|
|
||||||
## 🧑💻 Dependencies
|
## 🧑💻 Dependencies
|
||||||
|
|
||||||
|
@ -136,12 +136,7 @@ psql postgres -c "create database calckey with encoding = 'UTF8';"
|
||||||
|
|
||||||
## 🚚 Migrating from Misskey to Calckey
|
## 🚚 Migrating from Misskey to Calckey
|
||||||
|
|
||||||
> ⚠️ Because of their changes, migrating from Foundkey is not supported.
|
For migrating from Misskey v13, Misskey v12, and Foundkey, read [this document](./docs/migrate.md).
|
||||||
|
|
||||||
```sh
|
|
||||||
cp ../misskey/.config/default.yml ./.config/default.yml # replace `../misskey/` with misskey path, add `docker.env` if you use Docker
|
|
||||||
cp -r ../misskey/files .
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🍀 NGINX
|
## 🍀 NGINX
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# 🚚 Migrating from Misskey to Calckey
|
||||||
|
|
||||||
|
## Misskey v13 and above
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wget -O mkv13.patch https://codeberg.org/calckey/calckey/raw/branch/develop/docs/mkv13.patch
|
||||||
|
git apply mkv13.patch
|
||||||
|
|
||||||
|
cd packages/backend
|
||||||
|
|
||||||
|
LINE_NUM="$(npx typeorm migration:show -d ormconfig.js | grep -n activeEmailValidation1657346559800 | cut -d ':' -f 1)"
|
||||||
|
NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | nl)"
|
||||||
|
|
||||||
|
for i in $(seq 1 $NUM_MIGRAIONS); do
|
||||||
|
npx typeorm migration:revert -d ormconfig.js
|
||||||
|
done
|
||||||
|
|
||||||
|
git remote set-url origin https://codeberg.org/calckey/calckey.git
|
||||||
|
git fetch
|
||||||
|
git checkout main # or beta or develop
|
||||||
|
git pull --ff
|
||||||
|
# build using prefered method
|
||||||
|
```
|
||||||
|
|
||||||
|
## Misskey v12.119 and before
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git remote set-url origin https://codeberg.org/calckey/calckey.git
|
||||||
|
git fetch
|
||||||
|
git checkout main # or beta or develop
|
||||||
|
git pull --ff
|
||||||
|
# build using prefered method
|
||||||
|
```
|
||||||
|
|
||||||
|
## Foundkey
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd packages/backend
|
||||||
|
|
||||||
|
LINE_NUM="$(npx typeorm migration:show -d ormconfig.js | grep -n uniformThemecolor1652859567549 | cut -d ':' -f 1)"
|
||||||
|
NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | nl)"
|
||||||
|
|
||||||
|
for i in $(seq 1 $NUM_MIGRAIONS); do
|
||||||
|
npx typeorm migration:revert -d ormconfig.js
|
||||||
|
done
|
||||||
|
|
||||||
|
git remote set-url origin https://codeberg.org/calckey/calckey.git
|
||||||
|
git fetch
|
||||||
|
git checkout main # or beta or develop
|
||||||
|
git pull --ff
|
||||||
|
# build using prefered method
|
||||||
|
```
|
|
@ -0,0 +1,45 @@
|
||||||
|
diff --git a/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js b/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js
|
||||||
|
index 38a676985..c4ae690e0 100644
|
||||||
|
--- a/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js
|
||||||
|
+++ b/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js
|
||||||
|
@@ -6,6 +6,8 @@ export class removeLastCommunicatedAt1672704017999 {
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
- await queryRunner.query(`ALTER TABLE "instance" ADD "lastCommunicatedAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
|
||||||
|
+ await queryRunner.query(`ALTER TABLE "instance" ADD "lastCommunicatedAt" TIMESTAMP WITH TIME ZONE`);
|
||||||
|
+ await queryRunner.query(`UPDATE "instance" SET "lastCommunicatedAt" = COALESCE("infoUpdatedAt", "caughtAt")`);
|
||||||
|
+ await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "lastCommunicatedAt" SET NOT NULL`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diff --git a/packages/backend/migration/1673336077243-PollChoiceLength.js b/packages/backend/migration/1673336077243-PollChoiceLength.js
|
||||||
|
index 810c626e0..5809528cb 100644
|
||||||
|
--- a/packages/backend/migration/1673336077243-PollChoiceLength.js
|
||||||
|
+++ b/packages/backend/migration/1673336077243-PollChoiceLength.js
|
||||||
|
@@ -6,6 +6,6 @@ export class PollChoiceLength1673336077243 {
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
- await queryRunner.query(`ALTER TABLE "poll" ALTER COLUMN "choices" TYPE character varying(128) array`);
|
||||||
|
+ //await queryRunner.query(`ALTER TABLE "poll" ALTER COLUMN "choices" TYPE character varying(128) array`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diff --git a/packages/backend/migration/1674118260469-achievement.js b/packages/backend/migration/1674118260469-achievement.js
|
||||||
|
index 131ab96f8..57a922f83 100644
|
||||||
|
--- a/packages/backend/migration/1674118260469-achievement.js
|
||||||
|
+++ b/packages/backend/migration/1674118260469-achievement.js
|
||||||
|
@@ -18,12 +18,13 @@ export class achievement1674118260469 {
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'pollEnded')`);
|
||||||
|
+ await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
|
||||||
|
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
|
||||||
|
- await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
|
||||||
|
+ await queryRunner.query(`DELETE FROM "public"."notification" WHERE "type" = 'achievementEarned'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
|
||||||
|
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);
|
|
@ -7,9 +7,10 @@ import Router from "@koa/router";
|
||||||
import multer from "@koa/multer";
|
import multer from "@koa/multer";
|
||||||
import bodyParser from "koa-bodyparser";
|
import bodyParser from "koa-bodyparser";
|
||||||
import cors from "@koa/cors";
|
import cors from "@koa/cors";
|
||||||
import { apiMastodonCompatible } from "./mastodon/ApiMastodonCompatibleService.js";
|
import { apiMastodonCompatible, getClient } from "./mastodon/ApiMastodonCompatibleService.js";
|
||||||
import { Instances, AccessTokens, Users } from "@/models/index.js";
|
import { Instances, AccessTokens, Users } from "@/models/index.js";
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
|
import fs from "fs";
|
||||||
import endpoints from "./endpoints.js";
|
import endpoints from "./endpoints.js";
|
||||||
import compatibility from "./compatibility.js";
|
import compatibility from "./compatibility.js";
|
||||||
import handler from "./api-handler.js";
|
import handler from "./api-handler.js";
|
||||||
|
@ -39,6 +40,7 @@ app.use(async (ctx, next) => {
|
||||||
// Init router
|
// Init router
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
const mastoRouter = new Router();
|
const mastoRouter = new Router();
|
||||||
|
const mastoFileRouter = new Router();
|
||||||
const errorRouter = new Router();
|
const errorRouter = new Router();
|
||||||
|
|
||||||
// Init multer instance
|
// Init multer instance
|
||||||
|
@ -68,6 +70,46 @@ mastoRouter.use(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
let multipartData = await ctx.file;
|
||||||
|
if (!multipartData) {
|
||||||
|
ctx.body = { error: "No image" };
|
||||||
|
ctx.status = 401;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await client.uploadMedia(multipartData.buffer);
|
||||||
|
ctx.body = data.data;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
let multipartData = await ctx.file;
|
||||||
|
if (!multipartData) {
|
||||||
|
ctx.body = { error: "No image" };
|
||||||
|
ctx.status = 401;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await client.uploadMedia(multipartData.buffer);
|
||||||
|
ctx.body = data.data;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
mastoRouter.use(async (ctx, next) => {
|
mastoRouter.use(async (ctx, next) => {
|
||||||
if (ctx.request.query) {
|
if (ctx.request.query) {
|
||||||
if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) {
|
if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) {
|
||||||
|
@ -170,7 +212,9 @@ errorRouter.all("(.*)", async (ctx) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register router
|
// Register router
|
||||||
|
app.use(mastoFileRouter.routes());
|
||||||
app.use(mastoRouter.routes());
|
app.use(mastoRouter.routes());
|
||||||
|
app.use(mastoRouter.allowedMethods());
|
||||||
app.use(router.routes());
|
app.use(router.routes());
|
||||||
app.use(errorRouter.routes());
|
app.use(errorRouter.routes());
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import { Entity } from "@calckey/megalodon";
|
import { Entity } from "@calckey/megalodon";
|
||||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||||
|
import { Users, Notes } from "@/models/index.js";
|
||||||
|
import { IsNull, MoreThan } from "typeorm";
|
||||||
|
|
||||||
// TODO: add calckey features
|
// TODO: add calckey features
|
||||||
export async function getInstance(response: Entity.Instance) {
|
export async function getInstance(response: Entity.Instance) {
|
||||||
const meta = await fetchMeta(true);
|
const meta = await fetchMeta(true);
|
||||||
|
const totalUsers = Users.count({ where: { host: IsNull() } });
|
||||||
|
const totalStatuses = Notes.count({ where: { userHost: IsNull() } });
|
||||||
return {
|
return {
|
||||||
uri: response.uri,
|
uri: response.uri,
|
||||||
title: response.title || "",
|
title: response.title || "",
|
||||||
|
@ -12,7 +16,11 @@ export async function getInstance(response: Entity.Instance) {
|
||||||
email: response.email || "",
|
email: response.email || "",
|
||||||
version: "3.0.0 compatible (Calckey)",
|
version: "3.0.0 compatible (Calckey)",
|
||||||
urls: response.urls,
|
urls: response.urls,
|
||||||
stats: response.stats,
|
stats: {
|
||||||
|
user_count: (await totalUsers),
|
||||||
|
status_count: (await totalStatuses),
|
||||||
|
domain_count: response.stats.domain_count
|
||||||
|
},
|
||||||
thumbnail: response.thumbnail || "",
|
thumbnail: response.thumbnail || "",
|
||||||
languages: meta.langs,
|
languages: meta.langs,
|
||||||
registrations: !meta.disableRegistration || response.registrations,
|
registrations: !meta.disableRegistration || response.registrations,
|
||||||
|
@ -80,17 +88,17 @@ export async function getInstance(response: Entity.Instance) {
|
||||||
bot: true,
|
bot: true,
|
||||||
discoverable: false,
|
discoverable: false,
|
||||||
group: false,
|
group: false,
|
||||||
created_at: Math.floor(new Date().getTime() / 1000),
|
created_at: new Date().toISOString(),
|
||||||
note: "Please refer to the original instance for the actual admin contact.",
|
note: "<p>Please refer to the original instance for the actual admin contact.</p>",
|
||||||
url: "/",
|
url: `${response.uri}/`,
|
||||||
avatar: "/static-assets/badges/info.png",
|
avatar: `${response.uri}/static-assets/badges/info.png`,
|
||||||
avatar_static: "/static-assets/badges/info.png",
|
avatar_static: `${response.uri}/static-assets/badges/info.png`,
|
||||||
header: "https://http.cat/404",
|
header: "https://http.cat/404",
|
||||||
header_static: "https://http.cat/404",
|
header_static: "https://http.cat/404",
|
||||||
followers_count: -1,
|
followers_count: -1,
|
||||||
following_count: 0,
|
following_count: 0,
|
||||||
statuses_count: 0,
|
statuses_count: 0,
|
||||||
last_status_at: Math.floor(new Date().getTime() / 1000),
|
last_status_at: new Date().toISOString(),
|
||||||
noindex: true,
|
noindex: true,
|
||||||
emojis: [],
|
emojis: [],
|
||||||
fields: [],
|
fields: [],
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
import Router from "@koa/router";
|
import Router from "@koa/router";
|
||||||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
|
||||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||||
import fs from "fs";
|
import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
|
||||||
import { pipeline } from "node:stream";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
import { createTemp } from "@/misc/create-temp.js";
|
|
||||||
import { emojiRegex, emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
const pump = promisify(pipeline);
|
|
||||||
|
|
||||||
export function apiStatusMastodon(router: Router): void {
|
export function apiStatusMastodon(router: Router): void {
|
||||||
router.post("/v1/statuses", async (ctx, reply) => {
|
router.post("/v1/statuses", async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -52,7 +46,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
});
|
});
|
||||||
router.get<{ Params: { id: string } }>(
|
router.get<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id",
|
"/v1/statuses/:id",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -68,7 +62,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
);
|
);
|
||||||
router.delete<{ Params: { id: string } }>(
|
router.delete<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id",
|
"/v1/statuses/:id",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -90,7 +84,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
}
|
}
|
||||||
router.get<{ Params: { id: string } }>(
|
router.get<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id/context",
|
"/v1/statuses/:id/context",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -123,7 +117,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
);
|
);
|
||||||
router.get<{ Params: { id: string } }>(
|
router.get<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id/reblogged_by",
|
"/v1/statuses/:id/reblogged_by",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -139,13 +133,13 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
);
|
);
|
||||||
router.get<{ Params: { id: string } }>(
|
router.get<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id/favourited_by",
|
"/v1/statuses/:id/favourited_by",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
ctx.body = [];
|
ctx.body = [];
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
router.post<{ Params: { id: string } }>(
|
router.post<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id/favourite",
|
"/v1/statuses/:id/favourite",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -167,7 +161,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
);
|
);
|
||||||
router.post<{ Params: { id: string } }>(
|
router.post<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id/unfavourite",
|
"/v1/statuses/:id/unfavourite",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -185,7 +179,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
|
|
||||||
router.post<{ Params: { id: string } }>(
|
router.post<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id/reblog",
|
"/v1/statuses/:id/reblog",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -202,7 +196,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
|
|
||||||
router.post<{ Params: { id: string } }>(
|
router.post<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id/unreblog",
|
"/v1/statuses/:id/unreblog",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -219,7 +213,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
|
|
||||||
router.post<{ Params: { id: string } }>(
|
router.post<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id/bookmark",
|
"/v1/statuses/:id/bookmark",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -236,7 +230,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
|
|
||||||
router.post<{ Params: { id: string } }>(
|
router.post<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id/unbookmark",
|
"/v1/statuses/:id/unbookmark",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -253,7 +247,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
|
|
||||||
router.post<{ Params: { id: string } }>(
|
router.post<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id/pin",
|
"/v1/statuses/:id/pin",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -270,7 +264,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
|
|
||||||
router.post<{ Params: { id: string } }>(
|
router.post<{ Params: { id: string } }>(
|
||||||
"/v1/statuses/:id/unpin",
|
"/v1/statuses/:id/unpin",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -284,51 +278,9 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
router.post("/v1/media", async (ctx, reply) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const multipartData = await ctx.file;
|
|
||||||
if (!multipartData) {
|
|
||||||
ctx.body = { error: "No image" };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const [path] = await createTemp();
|
|
||||||
await pump(multipartData.buffer, fs.createWriteStream(path));
|
|
||||||
const image = fs.readFileSync(path);
|
|
||||||
const data = await client.uploadMedia(image);
|
|
||||||
ctx.body = data.data;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
router.post("/v2/media", async (ctx, reply) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const multipartData = await ctx.file;
|
|
||||||
if (!multipartData) {
|
|
||||||
ctx.body = { error: "No image" };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const [path] = await createTemp();
|
|
||||||
await pump(multipartData.buffer, fs.createWriteStream(path));
|
|
||||||
const image = fs.readFileSync(path);
|
|
||||||
const data = await client.uploadMedia(image);
|
|
||||||
ctx.body = data.data;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
router.get<{ Params: { id: string } }>(
|
router.get<{ Params: { id: string } }>(
|
||||||
"/v1/media/:id",
|
"/v1/media/:id",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -344,7 +296,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
);
|
);
|
||||||
router.put<{ Params: { id: string } }>(
|
router.put<{ Params: { id: string } }>(
|
||||||
"/v1/media/:id",
|
"/v1/media/:id",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -363,7 +315,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
);
|
);
|
||||||
router.get<{ Params: { id: string } }>(
|
router.get<{ Params: { id: string } }>(
|
||||||
"/v1/polls/:id",
|
"/v1/polls/:id",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
@ -379,7 +331,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
);
|
);
|
||||||
router.post<{ Params: { id: string } }>(
|
router.post<{ Params: { id: string } }>(
|
||||||
"/v1/polls/:id/votes",
|
"/v1/polls/:id/votes",
|
||||||
async (ctx, reply) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
|
|
@ -163,7 +163,8 @@ const isDeleted = ref(false);
|
||||||
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
|
||||||
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
'r': () => reply(true),
|
||||||
|
|
|
@ -187,7 +187,7 @@ const isDeleted = ref(false);
|
||||||
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
const conversation = ref<misskey.entities.Note[]>([]);
|
const conversation = ref<misskey.entities.Note[]>([]);
|
||||||
const replies = ref<misskey.entities.Note[]>([]);
|
const replies = ref<misskey.entities.Note[]>([]);
|
||||||
|
|
|
@ -1,45 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
|
<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
|
||||||
<div class="gqyayizv _popup">
|
<div class="_popup" :class="$style.root">
|
||||||
<button key="public" class="_button" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')">
|
<button key="public" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
|
||||||
<div><i class="ph-planet-bold ph-lg"></i></div>
|
<div :class="$style.icon"><i class="ph-planet-bold ph-lg"></i></div>
|
||||||
<div>
|
<div :class="$style.body">
|
||||||
<span>{{ i18n.ts._visibility.public }}</span>
|
<span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span>
|
||||||
<span>{{ i18n.ts._visibility.publicDescription }}</span>
|
<span :class="$style.itemDescription">{{ i18n.ts._visibility.publicDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button key="home" class="_button" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')">
|
<button key="home" class="_button" :class="[$style.item, { [$style.active]: v === 'home' }]" data-index="2" @click="choose('home')">
|
||||||
<div><i class="ph-house-bold ph-lg"></i></div>
|
<div :class="$style.icon"><i class="ph-house-bold ph-lg"></i></div>
|
||||||
<div>
|
<div :class="$style.body">
|
||||||
<span>{{ i18n.ts._visibility.home }}</span>
|
<span :class="$style.itemTitle">{{ i18n.ts._visibility.home }}</span>
|
||||||
<span>{{ i18n.ts._visibility.homeDescription }}</span>
|
<span :class="$style.itemDescription">{{ i18n.ts._visibility.homeDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button key="followers" class="_button" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')">
|
<button key="followers" class="_button" :class="[$style.item, { [$style.active]: v === 'followers' }]" data-index="3" @click="choose('followers')">
|
||||||
<div><i class="ph-lock-simple-open-bold ph-lg"></i></div>
|
<div :class="$style.icon"><i class="ph-lock-simple-open-bold ph-lg"></i></div>
|
||||||
<div>
|
<div :class="$style.body">
|
||||||
<span>{{ i18n.ts._visibility.followers }}</span>
|
<span :class="$style.itemTitle">{{ i18n.ts._visibility.followers }}</span>
|
||||||
<span>{{ i18n.ts._visibility.followersDescription }}</span>
|
<span :class="$style.itemDescription">{{ i18n.ts._visibility.followersDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button key="specified" :disabled="localOnly" class="_button" :class="{ active: v === 'specified' }" data-index="4" @click="choose('specified')">
|
<button key="specified" :disabled="localOnly" class="_button" :class="[$style.item, { [$style.active]: v === 'specified' }]" data-index="4" @click="choose('specified')">
|
||||||
<div><i class="ph-envelope-simple-open-bold ph-lg"></i></div>
|
<div :class="$style.icon"><i class="ph-envelope-simple-open-bold ph-lg"></i></div>
|
||||||
<div>
|
<div :class="$style.body">
|
||||||
<span>{{ i18n.ts._visibility.specified }}</span>
|
<span :class="$style.itemTitle">{{ i18n.ts._visibility.specified }}</span>
|
||||||
<span>{{ i18n.ts._visibility.specifiedDescription }}</span>
|
<span :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div class="divider"></div>
|
<div :class="$style.divider"></div>
|
||||||
<button key="localOnly" class="_button localOnly" :class="{ active: localOnly }" data-index="5" @click="localOnly = !localOnly">
|
<button key="localOnly" class="_button" :class="[$style.item, $style.localOnly, { [$style.active]: localOnly }]" data-index="5" @click="localOnly = !localOnly">
|
||||||
<div><i class="ph-hand-fist-bold ph-lg"></i></div>
|
<div :class="$style.icon"><i class="ph-hand-fist-bold ph-lg"></i></div>
|
||||||
<div>
|
<div :class="$style.body">
|
||||||
<span>{{ i18n.ts._visibility.localOnly }}</span>
|
<span :class="$style.itemTitle">{{ i18n.ts._visibility.localOnly }}</span>
|
||||||
<span>{{ i18n.ts._visibility.localOnlyDescription }}</span>
|
<span :class="$style.itemDescription">{{ i18n.ts._visibility.localOnlyDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div><i :class="localOnly ? 'ph-toggle-right-bold ph-lg' : 'ph-toggle-left-bold ph-lg'"></i></div>
|
<div :class="$style.toggle"><i :class="localOnly ? 'ph-toggle-right-bold ph-lg' : 'ph-toggle-left-bold ph-lg'"></i></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -48,7 +48,7 @@ import * as misskey from 'calckey-js';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const modal = $ref<InstanceType<typeof MkModal>>();
|
const modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
currentVisibility: typeof misskey.noteVisibilities[number];
|
currentVisibility: typeof misskey.noteVisibilities[number];
|
||||||
|
@ -79,81 +79,81 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.gqyayizv {
|
.root {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
> .divider {
|
.divider {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
> button {
|
&:active {
|
||||||
display: flex;
|
background: rgba(0, 0, 0, 0.1);
|
||||||
padding: 8px 14px;
|
}
|
||||||
font-size: 12px;
|
|
||||||
text-align: left;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
&:hover {
|
&.active {
|
||||||
background: rgba(0, 0, 0, 0.05);
|
color: var(--fgOnAccent);
|
||||||
}
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
&:active {
|
&.localOnly.active {
|
||||||
background: rgba(0, 0, 0, 0.1);
|
color: var(--accent);
|
||||||
}
|
background: inherit;
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: var(--fgOnAccent);
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.localOnly.active {
|
|
||||||
color: var(--accent);
|
|
||||||
background: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
> *:nth-child(1) {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 10px;
|
|
||||||
width: 16px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
> *:nth-child(2) {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
> span:first-child {
|
|
||||||
display: block;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
> span:last-child:not(:first-child) {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> *:nth-child(3) {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 10px;
|
|
||||||
width: 16px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 16px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemTitle {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemDescription {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
width: 16px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue