diff --git a/package.json b/package.json index c2f842a84..7cd7efc28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "calckey", - "version": "13.1.3-rc", + "version": "13.2.0-dev", "codename": "aqua", "repository": { "type": "git", diff --git a/packages/backend/assets/favicon.svg b/packages/backend/assets/favicon.svg new file mode 100644 index 000000000..a77c6e22b Binary files /dev/null and b/packages/backend/assets/favicon.svg differ diff --git a/packages/backend/assets/inverse wordmark.svg b/packages/backend/assets/inverse wordmark.svg new file mode 100644 index 000000000..07e9ceeee Binary files /dev/null and b/packages/backend/assets/inverse wordmark.svg differ diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index 37c5031c0..882cfdd31 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -197,7 +197,10 @@ export const NoteRepository = db.getRepository(Note).extend({ .map((x) => decodeReaction(x).reaction) .map((x) => x.replace(/:/g, "")); - const noteEmoji = await populateEmojis(note.emojis.concat(reactionEmojiNames), host); + const noteEmoji = await populateEmojis( + note.emojis.concat(reactionEmojiNames), + host, + ); const reactionEmoji = await populateEmojis(reactionEmojiNames, host); const packed: Packed<"Note"> = await awaitAll({ id: note.id, diff --git a/packages/backend/src/models/schema/note.ts b/packages/backend/src/models/schema/note.ts index 6bc8443f0..e17f054e8 100644 --- a/packages/backend/src/models/schema/note.ts +++ b/packages/backend/src/models/schema/note.ts @@ -161,8 +161,9 @@ export const packedNoteSchema = { nullable: false, }, emojis: { - type: 'object', - optional: true, nullable: true, + type: "object", + optional: true, + nullable: true, }, reactions: { type: "object", diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index ee9ccf52a..3c438aa2f 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -111,6 +111,16 @@ export async function createNote( const note: IPost = object; + if (note.id && !note.id.startsWith("https://")) { + throw new Error(`unexpected shcema of note.id: ${note.id}`); + } + + const url = getOneApHrefNullable(note.url); + + if (url && !url.startsWith("https://")) { + throw new Error(`unexpected shcema of note url: ${url}`); + } + logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); logger.info(`Creating the Note: ${note.id}`); @@ -140,7 +150,9 @@ export async function createNote( // Skip if author is suspended. if (actor.isSuspended) { - logger.debug(`User ${actor.usernameLower}@${actor.host} suspended; discarding.`) + logger.debug( + `User ${actor.usernameLower}@${actor.host} suspended; discarding.`, + ); return null; } @@ -362,7 +374,7 @@ export async function createNote( apEmojis, poll, uri: note.id, - url: getOneApHrefNullable(note.url), + url: url, }, silent, ); diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 0ec671f0a..372ef99e1 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -195,6 +195,12 @@ export async function createPerson( const bday = person["vcard:bday"]?.match(/^\d{4}-\d{2}-\d{2}/); + const url = getOneApHrefNullable(person.url); + + if (url && !url.startsWith("https://")) { + throw new Error(`unexpected shcema of person url: ${url}`); + } + // Create user let user: IRemoteUser; try { @@ -237,7 +243,7 @@ export async function createPerson( description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, - url: getOneApHrefNullable(person.url), + url: url, fields, birthday: bday ? bday[0] : null, location: person["vcard:Address"] || null, @@ -387,6 +393,12 @@ export async function updatePerson( const bday = person["vcard:bday"]?.match(/^\d{4}-\d{2}-\d{2}/); + const url = getOneApHrefNullable(person.url); + + if (url && !url.startsWith("https://")) { + throw new Error(`unexpected shcema of person url: ${url}`); + } + const updates = { lastFetchedAt: new Date(), inbox: person.inbox, @@ -430,7 +442,7 @@ export async function updatePerson( await UserProfiles.update( { userId: exist.id }, { - url: getOneApHrefNullable(person.url), + url: url, fields, description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 353f137a7..6ee1a977e 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -198,7 +198,7 @@ import * as ep___i_readAnnouncement from "./endpoints/i/read-announcement.js"; import * as ep___i_regenerateToken from "./endpoints/i/regenerate-token.js"; import * as ep___i_registry_getAll from "./endpoints/i/registry/get-all.js"; import * as ep___i_registry_getDetail from "./endpoints/i/registry/get-detail.js"; -import * as ep___i_registry_getUnsecure from './endpoints/i/registry/get-unsecure.js'; +import * as ep___i_registry_getUnsecure from "./endpoints/i/registry/get-unsecure.js"; import * as ep___i_registry_get from "./endpoints/i/registry/get.js"; import * as ep___i_registry_keysWithType from "./endpoints/i/registry/keys-with-type.js"; import * as ep___i_registry_keys from "./endpoints/i/registry/keys.js"; @@ -767,10 +767,10 @@ export interface IEndpointMeta { } export interface IEndpoint { - name: string, - exec: any, // TODO: may be obosolete @ThatOneCalculator - meta: IEndpointMeta, - params: Schema, + name: string; + exec: any; // TODO: may be obosolete @ThatOneCalculator + meta: IEndpointMeta; + params: Schema; } const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts index 40065c83e..f98c6c929 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts @@ -1,6 +1,6 @@ import { ApiError } from "../../../error.js"; import define from "../../../define.js"; -import { RegistryItems } from "../../../../../models/index.js"; +import { RegistryItems } from "@/models/index.js"; export const meta = { requireCredential: true, diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts index da98a9df1..189705903 100644 --- a/packages/backend/src/server/api/index.ts +++ b/packages/backend/src/server/api/index.ts @@ -7,7 +7,7 @@ import Router from "@koa/router"; import multer from "@koa/multer"; import bodyParser from "koa-bodyparser"; import cors from "@koa/cors"; -import { apiMastodonCompatible } from './mastodon/ApiMastodonCompatibleService.js'; +import { apiMastodonCompatible } from "./mastodon/ApiMastodonCompatibleService.js"; import { Instances, AccessTokens, Users } from "@/models/index.js"; import config from "@/config/index.js"; import endpoints from "./endpoints.js"; @@ -19,6 +19,7 @@ import signupPending from "./private/signup-pending.js"; import discord from "./service/discord.js"; import github from "./service/github.js"; import twitter from "./service/twitter.js"; +import { koaBody } from "koa-body"; // Init app const app = new Koa(); @@ -35,16 +36,10 @@ app.use(async (ctx, next) => { await next(); }); -app.use( - bodyParser({ - // リクエストが multipart/form-data でない限りはJSONだと見なす - detectJSON: (ctx) => - !( - ctx.is("multipart/form-data") || - ctx.is("application/x-www-form-urlencoded") - ), - }), -); +// Init router +const router = new Router(); +const mastoRouter = new Router(); +const errorRouter = new Router(); // Init multer instance const upload = multer({ @@ -55,10 +50,23 @@ const upload = multer({ }, }); -// Init router -const router = new Router(); +router.use( + bodyParser({ + // リクエストが multipart/form-data でない限りはJSONだと見なす + detectJSON: (ctx) => + !( + ctx.is("multipart/form-data") || + ctx.is("application/x-www-form-urlencoded") + ), + }), +); -apiMastodonCompatible(router); +mastoRouter.use(koaBody({ + multipart: true, + urlencoded: true +})); + +apiMastodonCompatible(mastoRouter); /** * Register endpoint handlers @@ -144,11 +152,13 @@ router.post("/miauth/:session/check", async (ctx) => { }); // Return 404 for unknown API -router.all("(.*)", async (ctx) => { +errorRouter.all("(.*)", async (ctx) => { ctx.status = 404; }); // Register router +app.use(mastoRouter.routes()); app.use(router.routes()); +app.use(errorRouter.routes()); export default app; diff --git a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts index 57a86c96d..ddd2a23f0 100644 --- a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts +++ b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts @@ -1,32 +1,39 @@ import Router from "@koa/router"; -import megalodon, { MegalodonInterface } from '@cutls/megalodon'; -import { apiAuthMastodon } from './endpoints/auth.js'; -import { apiAccountMastodon } from './endpoints/account.js'; -import { apiStatusMastodon } from './endpoints/status.js'; -import { apiFilterMastodon } from './endpoints/filter.js'; -import { apiTimelineMastodon } from './endpoints/timeline.js'; -import { apiNotificationsMastodon } from './endpoints/notifications.js'; -import { apiSearchMastodon } from './endpoints/search.js'; -import { getInstance } from './endpoints/meta.js'; +import megalodon, { MegalodonInterface } from "@cutls/megalodon"; +import { apiAuthMastodon } from "./endpoints/auth.js"; +import { apiAccountMastodon } from "./endpoints/account.js"; +import { apiStatusMastodon } from "./endpoints/status.js"; +import { apiFilterMastodon } from "./endpoints/filter.js"; +import { apiTimelineMastodon } from "./endpoints/timeline.js"; +import { apiNotificationsMastodon } from "./endpoints/notifications.js"; +import { apiSearchMastodon } from "./endpoints/search.js"; +import { getInstance } from "./endpoints/meta.js"; -export function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface { - const accessTokenArr = authorization?.split(' ') ?? [null]; +export function getClient( + BASE_URL: string, + authorization: string | undefined, +): MegalodonInterface { + const accessTokenArr = authorization?.split(" ") ?? [null]; const accessToken = accessTokenArr[accessTokenArr.length - 1]; - const generator = (megalodon as any).default - const client = generator('misskey', BASE_URL, accessToken) as MegalodonInterface; - return client + const generator = (megalodon as any).default; + const client = generator( + "misskey", + BASE_URL, + accessToken, + ) as MegalodonInterface; + return client; } export function apiMastodonCompatible(router: Router): void { - apiAuthMastodon(router) - apiAccountMastodon(router) - apiStatusMastodon(router) - apiFilterMastodon(router) - apiTimelineMastodon(router) - apiNotificationsMastodon(router) - apiSearchMastodon(router) + apiAuthMastodon(router); + apiAccountMastodon(router); + apiStatusMastodon(router); + apiFilterMastodon(router); + apiTimelineMastodon(router); + apiNotificationsMastodon(router); + apiSearchMastodon(router); - router.get('/v1/custom_emojis', async (ctx) => { + router.get("/v1/custom_emojis", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); @@ -34,25 +41,24 @@ export function apiMastodonCompatible(router: Router): void { const data = await client.getInstanceCustomEmojis(); ctx.body = data.data; } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - router.get('/v1/instance', async (ctx) => { + router.get("/v1/instance", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt - // displayed without being logged in + const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt + // displayed without being logged in try { const data = await client.getInstance(); ctx.body = getInstance(data.data); } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - -} +} diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index 61d4da8a8..0162951d6 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -1,323 +1,376 @@ -import megalodon, { MegalodonInterface } from '@cutls/megalodon'; +import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import Router from "@koa/router"; -import { koaBody } from 'koa-body'; -import { getClient } from '../ApiMastodonCompatibleService.js'; -import { toLimitToInt } from './timeline.js'; +import { koaBody } from "koa-body"; +import { getClient } from "../ApiMastodonCompatibleService.js"; +import { toLimitToInt } from "./timeline.js"; export function apiAccountMastodon(router: Router): void { - - router.get('/v1/accounts/verify_credentials', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.verifyAccountCredentials(); - const acct = data.data; - acct.url = `${BASE_URL}/@${acct.url}` - acct.note = '' - acct.avatar_static = acct.avatar - acct.header = acct.header || '' - acct.header_static = acct.header || '' - acct.source = { - note: acct.note, - fields: acct.fields, - privacy: 'public', - sensitive: false, - language: '' - } - ctx.body = acct - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.patch('/v1/accounts/update_credentials', async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.updateCredentials((ctx.request as any).body as any); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>('/v1/accounts/:id', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccount(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>('/v1/accounts/:id/statuses', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountStatuses(ctx.params.id, toLimitToInt(ctx.query as any)); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>('/v1/accounts/:id/followers', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountFollowers(ctx.params.id, ctx.query as any); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>('/v1/accounts/:id/following', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountFollowing(ctx.params.id, ctx.query as any); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>('/v1/accounts/:id/lists', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountLists(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>('/v1/accounts/:id/follow', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.followAccount(ctx.params.id); - const acct = data.data; - acct.following = true; - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>('/v1/accounts/:id/unfollow', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unfollowAccount(ctx.params.id); - const acct = data.data; - acct.following = false; - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>('/v1/accounts/:id/block', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.blockAccount(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>('/v1/accounts/:id/unblock', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unblockAccount(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>('/v1/accounts/:id/mute', async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.muteAccount(ctx.params.id, (ctx.request as any).body as any); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>('/v1/accounts/:id/unmute', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unmuteAccount(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.get('/v1/accounts/relationships', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const idsRaw = (ctx.query as any)['id[]'] - const ids = typeof idsRaw === 'string' ? [idsRaw] : idsRaw - const data = await client.getRelationships(ids) as any; - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.get('/v1/bookmarks', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getBookmarks(ctx.query as any) as any; - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.get('/v1/favourites', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getFavourites(ctx.query as any); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.get('/v1/mutes', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getMutes(ctx.query as any); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.get('/v1/blocks', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getBlocks(ctx.query as any); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.get('/v1/follow_ctxs', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getFollowRequests((ctx.query as any || { limit: 20 }).limit); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>('/v1/follow_ctxs/:id/authorize', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.acceptFollowRequest(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>('/v1/follow_ctxs/:id/reject', async (ctx, next) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.rejectFollowRequest(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status =(401); - ctx.body = e.response.data; - } - }); - -} + router.get("/v1/accounts/verify_credentials", async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.verifyAccountCredentials(); + const acct = data.data; + acct.url = `${BASE_URL}/@${acct.url}`; + acct.note = ""; + acct.avatar_static = acct.avatar; + acct.header = acct.header || ""; + acct.header_static = acct.header || ""; + acct.source = { + note: acct.note, + fields: acct.fields, + privacy: "public", + sensitive: false, + language: "", + }; + ctx.body = acct; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.patch("/v1/accounts/update_credentials", async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.updateCredentials( + (ctx.request as any).body as any, + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get<{ Params: { id: string } }>( + "/v1/accounts/:id", + async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getAccount(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { id: string } }>( + "/v1/accounts/:id/statuses", + async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getAccountStatuses( + ctx.params.id, + toLimitToInt(ctx.query as any), + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { id: string } }>( + "/v1/accounts/:id/followers", + async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getAccountFollowers( + ctx.params.id, + ctx.query as any, + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { id: string } }>( + "/v1/accounts/:id/following", + async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getAccountFollowing( + ctx.params.id, + ctx.query as any, + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { id: string } }>( + "/v1/accounts/:id/lists", + async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getAccountLists(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/accounts/:id/follow", + async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.followAccount(ctx.params.id); + const acct = data.data; + acct.following = true; + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/accounts/:id/unfollow", + async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.unfollowAccount(ctx.params.id); + const acct = data.data; + acct.following = false; + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/accounts/:id/block", + async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.blockAccount(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/accounts/:id/unblock", + async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.unblockAccount(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/accounts/:id/mute", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.muteAccount( + ctx.params.id, + (ctx.request as any).body as any, + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/accounts/:id/unmute", + async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.unmuteAccount(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get("/v1/accounts/relationships", async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const idsRaw = (ctx.query as any)["id[]"]; + const ids = typeof idsRaw === "string" ? [idsRaw] : idsRaw; + const data = (await client.getRelationships(ids)) as any; + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get("/v1/bookmarks", async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = (await client.getBookmarks(ctx.query as any)) as any; + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get("/v1/favourites", async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getFavourites(ctx.query as any); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get("/v1/mutes", async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getMutes(ctx.query as any); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get("/v1/blocks", async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getBlocks(ctx.query as any); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get("/v1/follow_ctxs", async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getFollowRequests( + ((ctx.query as any) || { limit: 20 }).limit, + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.post<{ Params: { id: string } }>( + "/v1/follow_ctxs/:id/authorize", + async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.acceptFollowRequest(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/follow_ctxs/:id/reject", + async (ctx, next) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.rejectFollowRequest(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); +} diff --git a/packages/backend/src/server/api/mastodon/endpoints/auth.ts b/packages/backend/src/server/api/mastodon/endpoints/auth.ts index ff8b8a518..46b013f3f 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/auth.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/auth.ts @@ -1,81 +1,84 @@ -import megalodon, { MegalodonInterface } from '@cutls/megalodon'; +import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import Router from "@koa/router"; -import { koaBody } from 'koa-body'; -import { getClient } from '../ApiMastodonCompatibleService.js'; +import { koaBody } from "koa-body"; +import { getClient } from "../ApiMastodonCompatibleService.js"; +import bodyParser from "koa-bodyparser"; const readScope = [ - 'read:account', - 'read:drive', - 'read:blocks', - 'read:favorites', - 'read:following', - 'read:messaging', - 'read:mutes', - 'read:notifications', - 'read:reactions', - 'read:pages', - 'read:page-likes', - 'read:user-groups', - 'read:channels', - 'read:gallery', - 'read:gallery-likes' -] + "read:account", + "read:drive", + "read:blocks", + "read:favorites", + "read:following", + "read:messaging", + "read:mutes", + "read:notifications", + "read:reactions", + "read:pages", + "read:page-likes", + "read:user-groups", + "read:channels", + "read:gallery", + "read:gallery-likes", +]; const writeScope = [ - 'write:account', - 'write:drive', - 'write:blocks', - 'write:favorites', - 'write:following', - 'write:messaging', - 'write:mutes', - 'write:notes', - 'write:notifications', - 'write:reactions', - 'write:votes', - 'write:pages', - 'write:page-likes', - 'write:user-groups', - 'write:channels', - 'write:gallery', - 'write:gallery-likes' -] + "write:account", + "write:drive", + "write:blocks", + "write:favorites", + "write:following", + "write:messaging", + "write:mutes", + "write:notes", + "write:notifications", + "write:reactions", + "write:votes", + "write:pages", + "write:page-likes", + "write:user-groups", + "write:channels", + "write:gallery", + "write:gallery-likes", +]; export function apiAuthMastodon(router: Router): void { - - router.post('/v1/apps', async (ctx) => { + router.post("/v1/apps", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); const body: any = ctx.request.body; try { - let scope = body.scopes - console.log(body) - if (typeof scope === 'string') scope = scope.split(' ') - const pushScope = new Set() + let scope = body.scopes; + console.log(body); + if (typeof scope === "string") scope = scope.split(" "); + const pushScope = new Set(); for (const s of scope) { - if (s.match(/^read/)) for (const r of readScope) pushScope.add(r) - if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r) + if (s.match(/^read/)) for (const r of readScope) pushScope.add(r); + if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r); } - const scopeArr = Array.from(pushScope) + const scopeArr = Array.from(pushScope); - let red = body.redirect_uris - if (red === 'urn:ietf:wg:oauth:2.0:oob') { - red = 'https://thedesk.top/hello.html' + let red = body.redirect_uris; + if (red === "urn:ietf:wg:oauth:2.0:oob") { + red = "https://thedesk.top/hello.html"; } - const appData = await client.registerApp(body.client_name, { scopes: scopeArr, redirect_uris: red, website: body.website }); + const appData = await client.registerApp(body.client_name, { + scopes: scopeArr, + redirect_uris: red, + website: body.website, + }); ctx.body = { id: appData.id, name: appData.name, website: appData.website, redirect_uri: red, - client_id: Buffer.from(appData.url || '').toString('base64'), + client_id: Buffer.from(appData.url || "").toString("base64"), client_secret: appData.clientSecret, - } + }; } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - } diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts index 810b8be11..6098a9543 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts @@ -1,11 +1,9 @@ -import megalodon, { MegalodonInterface } from '@cutls/megalodon'; +import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import Router from "@koa/router"; -import { koaBody } from 'koa-body'; -import { getClient } from '../ApiMastodonCompatibleService.js'; +import { getClient } from "../ApiMastodonCompatibleService.js"; export function apiFilterMastodon(router: Router): void { - - router.get('/v1/filters', async (ctx) => { + router.get("/v1/filters", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); @@ -14,13 +12,13 @@ export function apiFilterMastodon(router: Router): void { const data = await client.getFilters(); ctx.body = data.data; } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - router.get('/v1/filters/:id', async (ctx) => { + router.get("/v1/filters/:id", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); @@ -29,13 +27,13 @@ export function apiFilterMastodon(router: Router): void { const data = await client.getFilter(ctx.params.id); ctx.body = data.data; } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - router.post('/v1/filters', async (ctx) => { + router.post("/v1/filters", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); @@ -44,28 +42,32 @@ export function apiFilterMastodon(router: Router): void { const data = await client.createFilter(body.phrase, body.context, body); ctx.body = data.data; } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - router.post('/v1/filters/:id', async (ctx) => { + router.post("/v1/filters/:id", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); const body: any = ctx.request.body; try { - const data = await client.updateFilter(ctx.params.id, body.phrase, body.context); + const data = await client.updateFilter( + ctx.params.id, + body.phrase, + body.context, + ); ctx.body = data.data; } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - router.delete('/v1/filters/:id', async (ctx) => { + router.delete("/v1/filters/:id", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); @@ -74,10 +76,9 @@ export function apiFilterMastodon(router: Router): void { const data = await client.deleteFilter(ctx.params.id); ctx.body = data.data; } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - } diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts index 625ff386c..9c52414ad 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts @@ -1,16 +1,15 @@ -import megalodon, { MegalodonInterface } from '@cutls/megalodon'; +import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import Router from "@koa/router"; -import { koaBody } from 'koa-body'; -import { getClient } from '../ApiMastodonCompatibleService.js'; -import { toTextWithReaction } from './timeline.js'; +import { koaBody } from "koa-body"; +import { getClient } from "../ApiMastodonCompatibleService.js"; +import { toTextWithReaction } from "./timeline.js"; function toLimitToInt(q: any) { - if (q.limit) if (typeof q.limit === 'string') q.limit = parseInt(q.limit, 10) - return q + if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10); + return q; } export function apiNotificationsMastodon(router: Router): void { - - router.get('/v1/notifications', async (ctx) => { + router.get("/v1/notifications", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); @@ -19,23 +18,26 @@ export function apiNotificationsMastodon(router: Router): void { const data = await client.getNotifications(toLimitToInt(ctx.query)); const notfs = data.data; const ret = notfs.map((n) => { - if(n.type !== 'follow' && n.type !== 'follow_request') { - if (n.type === 'reaction') n.type = 'favourite' - n.status = toTextWithReaction(n.status ? [n.status] : [], ctx.hostname)[0] - return n - } else { - return n - } - }) + if (n.type !== "follow" && n.type !== "follow_request") { + if (n.type === "reaction") n.type = "favourite"; + n.status = toTextWithReaction( + n.status ? [n.status] : [], + ctx.hostname, + )[0]; + return n; + } else { + return n; + } + }); ctx.body = ret; } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - router.get('/v1/notification/:id', async (ctx) => { + router.get("/v1/notification/:id", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); @@ -43,20 +45,20 @@ export function apiNotificationsMastodon(router: Router): void { try { const dataRaw = await client.getNotification(ctx.params.id); const data = dataRaw.data; - if(data.type !== 'follow' && data.type !== 'follow_request') { - if (data.type === 'reaction') data.type = 'favourite' - ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0] + if (data.type !== "follow" && data.type !== "follow_request") { + if (data.type === "reaction") data.type = "favourite"; + ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0]; } else { - ctx.body = data + ctx.body = data; } } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - router.post('/v1/notifications/clear', async (ctx) => { + router.post("/v1/notifications/clear", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); @@ -65,13 +67,13 @@ export function apiNotificationsMastodon(router: Router): void { const data = await client.dismissNotifications(); ctx.body = data.data; } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - router.post('/v1/notification/:id/dismiss', async (ctx) => { + router.post("/v1/notification/:id/dismiss", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); @@ -80,10 +82,9 @@ export function apiNotificationsMastodon(router: Router): void { const data = await client.dismissNotification(ctx.params.id); ctx.body = data.data; } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - } diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts index dce3ff57c..48161733e 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/search.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts @@ -1,25 +1,22 @@ -import megalodon, { MegalodonInterface } from '@cutls/megalodon'; +import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import Router from "@koa/router"; -import { koaBody } from 'koa-body'; -import { getClient } from '../ApiMastodonCompatibleService.js'; +import { getClient } from "../ApiMastodonCompatibleService.js"; export function apiSearchMastodon(router: Router): void { - - router.get('/v1/search', async (ctx) => { + router.get("/v1/search", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; const client = getClient(BASE_URL, accessTokens); const body: any = ctx.request.body; try { - const query: any = ctx.query - const type = query.type || '' + const query: any = ctx.query; + const type = query.type || ""; const data = await client.search(query.q, type, query); ctx.body = data.data; } catch (e: any) { - console.error(e) + console.error(e); ctx.status = 401; ctx.body = e.response.data; } }); - } diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index 8dc4ba5f7..3afd7e576 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -1,403 +1,483 @@ import Router from "@koa/router"; -import { koaBody } from 'koa-body'; -import megalodon, { MegalodonInterface } from '@cutls/megalodon'; -import { getClient } from '../ApiMastodonCompatibleService.js'; -import fs from 'fs' -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 megalodon, { MegalodonInterface } from "@cutls/megalodon"; +import { getClient } from "../ApiMastodonCompatibleService.js"; +import fs from "fs"; +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"; const pump = promisify(pipeline); export function apiStatusMastodon(router: Router): void { - router.post('/v1/statuses', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const body: any = ctx.request.body - const text = body.status - const removed = text.replace(/@\S+/g, '').replaceAll(' ', '') - const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed) - const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed) - if (body.in_reply_to_id && isDefaultEmoji || isCustomEmoji) { - const a = await client.createEmojiReaction(body.in_reply_to_id, removed) - ctx.body = a.data - } - if (body.in_reply_to_id && removed === '/unreact') { - try { - const id = body.in_reply_to_id - const post = await client.getStatus(id) - const react = post.data.emoji_reactions.filter((e) => e.me)[0].name - const data = await client.deleteEmojiReaction(id, react); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - } - if (!body.media_ids) body.media_ids = undefined - if (body.media_ids && !body.media_ids.length) body.media_ids = undefined - const data = await client.postStatus(text, body); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>('/v1/statuses/:id', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.delete<{ Params: { id: string } }>('/v1/statuses/:id', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.deleteStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - interface IReaction { - id: string - createdAt: string - user: MisskeyEntity.User, - type: string - } - router.get<{ Params: { id: string } }>('/v1/statuses/:id/context', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const id = ctx.params.id - const data = await client.getStatusContext(id, ctx.query as any); - const status = await client.getStatus(id); - const reactionsAxios = await axios.get(`${BASE_URL}/api/notes/reactions?noteId=${id}`) - const reactions: IReaction[] = reactionsAxios.data - const text = reactions.map((r) => `${r.type.replace('@.', '')} ${r.user.username}`).join('
') - data.data.descendants.unshift(statusModel(status.data.id, status.data.account.id, status.data.emojis, text)) - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>('/v1/statuses/:id/reblogged_by', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getStatusRebloggedBy(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>('/v1/statuses/:id/favourited_by', async (ctx, reply) => { - ctx.body = [] - }); - router.post<{ Params: { id: string } }>('/v1/statuses/:id/favourite', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const react = await getFirstReaction(BASE_URL, accessTokens); - try { - const a = await client.createEmojiReaction(ctx.params.id, react) as any; - //const data = await client.favouriteStatus(ctx.params.id) as any; - ctx.body = a.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>('/v1/statuses/:id/unfavourite', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const react = await getFirstReaction(BASE_URL, accessTokens); - try { - const data = await client.deleteEmojiReaction(ctx.params.id, react); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); + router.post("/v1/statuses", async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const body: any = ctx.request.body; + const text = body.status; + const removed = text.replace(/@\S+/g, "").replaceAll(" ", ""); + const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed); + const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed); + if ((body.in_reply_to_id && isDefaultEmoji) || isCustomEmoji) { + const a = await client.createEmojiReaction( + body.in_reply_to_id, + removed, + ); + ctx.body = a.data; + } + if (body.in_reply_to_id && removed === "/unreact") { + try { + const id = body.in_reply_to_id; + const post = await client.getStatus(id); + const react = post.data.emoji_reactions.filter((e) => e.me)[0].name; + const data = await client.deleteEmojiReaction(id, react); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + } + if (!body.media_ids) body.media_ids = undefined; + if (body.media_ids && !body.media_ids.length) body.media_ids = undefined; + const data = await client.postStatus(text, body); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get<{ Params: { id: string } }>( + "/v1/statuses/:id", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getStatus(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.delete<{ Params: { id: string } }>( + "/v1/statuses/:id", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.deleteStatus(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + interface IReaction { + id: string; + createdAt: string; + user: MisskeyEntity.User; + type: string; + } + router.get<{ Params: { id: string } }>( + "/v1/statuses/:id/context", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const id = ctx.params.id; + const data = await client.getStatusContext(id, ctx.query as any); + const status = await client.getStatus(id); + const reactionsAxios = await axios.get( + `${BASE_URL}/api/notes/reactions?noteId=${id}`, + ); + const reactions: IReaction[] = reactionsAxios.data; + const text = reactions + .map((r) => `${r.type.replace("@.", "")} ${r.user.username}`) + .join("
"); + data.data.descendants.unshift( + statusModel( + status.data.id, + status.data.account.id, + status.data.emojis, + text, + ), + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { id: string } }>( + "/v1/statuses/:id/reblogged_by", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getStatusRebloggedBy(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { id: string } }>( + "/v1/statuses/:id/favourited_by", + async (ctx, reply) => { + ctx.body = []; + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/statuses/:id/favourite", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + const react = await getFirstReaction(BASE_URL, accessTokens); + try { + const a = (await client.createEmojiReaction( + ctx.params.id, + react, + )) as any; + //const data = await client.favouriteStatus(ctx.params.id) as any; + ctx.body = a.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/statuses/:id/unfavourite", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + const react = await getFirstReaction(BASE_URL, accessTokens); + try { + const data = await client.deleteEmojiReaction(ctx.params.id, react); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); - router.post<{ Params: { id: string } }>('/v1/statuses/:id/reblog', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.reblogStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); + router.post<{ Params: { id: string } }>( + "/v1/statuses/:id/reblog", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.reblogStatus(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); - router.post<{ Params: { id: string } }>('/v1/statuses/:id/unreblog', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unreblogStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); + router.post<{ Params: { id: string } }>( + "/v1/statuses/:id/unreblog", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.unreblogStatus(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); - router.post<{ Params: { id: string } }>('/v1/statuses/:id/bookmark', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.bookmarkStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); + router.post<{ Params: { id: string } }>( + "/v1/statuses/:id/bookmark", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.bookmarkStatus(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); - router.post<{ Params: { id: string } }>('/v1/statuses/:id/unbookmark', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unbookmarkStatus(ctx.params.id) as any; - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); + router.post<{ Params: { id: string } }>( + "/v1/statuses/:id/unbookmark", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = (await client.unbookmarkStatus(ctx.params.id)) as any; + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); - router.post<{ Params: { id: string } }>('/v1/statuses/:id/pin', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.pinStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - - router.post<{ Params: { id: string } }>('/v1/statuses/:id/unpin', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unpinStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - 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 } }>('/v1/media/:id', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getMedia(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.put<{ Params: { id: string } }>('/v1/media/:id', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.updateMedia(ctx.params.id, ctx.request.body as any); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>('/v1/polls/:id', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getPoll(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>('/v1/polls/:id/votes', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.votePoll(ctx.params.id, (ctx.request.body as any).choices); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - ctx.status = (401); - ctx.body = e.response.data; - } - }); + router.post<{ Params: { id: string } }>( + "/v1/statuses/:id/pin", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.pinStatus(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/statuses/:id/unpin", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.unpinStatus(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + 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 } }>( + "/v1/media/:id", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getMedia(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.put<{ Params: { id: string } }>( + "/v1/media/:id", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.updateMedia( + ctx.params.id, + ctx.request.body as any, + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { id: string } }>( + "/v1/polls/:id", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getPoll(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/polls/:id/votes", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.votePoll( + ctx.params.id, + (ctx.request.body as any).choices, + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); } -async function getFirstReaction(BASE_URL: string, accessTokens: string | undefined) { - const accessTokenArr = accessTokens?.split(' ') ?? [null]; - const accessToken = accessTokenArr[accessTokenArr.length - 1]; - let react = '👍' - try { - const api = await axios.post(`${BASE_URL}/api/i/registry/get-unsecure`, { - scope: ['client', 'base'], - key: 'reactions', - i: accessToken - }) - const reactRaw = api.data - react = Array.isArray(reactRaw) ? api.data[0] : '👍' - console.log(api.data) - return react - } catch (e) { - return react - } +async function getFirstReaction( + BASE_URL: string, + accessTokens: string | undefined, +) { + const accessTokenArr = accessTokens?.split(" ") ?? [null]; + const accessToken = accessTokenArr[accessTokenArr.length - 1]; + let react = "👍"; + try { + const api = await axios.post(`${BASE_URL}/api/i/registry/get-unsecure`, { + scope: ["client", "base"], + key: "reactions", + i: accessToken, + }); + const reactRaw = api.data; + react = Array.isArray(reactRaw) ? api.data[0] : "👍"; + console.log(api.data); + return react; + } catch (e) { + return react; + } } -export function statusModel(id: string | null, acctId: string | null, emojis: MastodonEntity.Emoji[], content: string) { - const now = "1970-01-02T00:00:00.000Z" - return { - id: '9atm5frjhb', - uri: 'https://http.cat/404', // "" - url: 'https://http.cat/404', // "", - account: { - id: '9arzuvv0sw', - username: 'ReactionBot', - acct: 'ReactionBot', - display_name: 'ReactionOfThisPost', - locked: false, - created_at: now, - followers_count: 0, - following_count: 0, - statuses_count: 0, - note: '', - url: 'https://http.cat/404', - avatar: 'https://http.cat/404', - avatar_static: 'https://http.cat/404', - header: 'https://http.cat/404', // "" - header_static: 'https://http.cat/404', // "" - emojis: [], - fields: [], - moved: null, - bot: false, - }, - in_reply_to_id: id, - in_reply_to_account_id: acctId, - reblog: null, - content: `

${content}

`, - plain_content: null, - created_at: now, - emojis: emojis, - replies_count: 0, - reblogs_count: 0, - favourites_count: 0, - favourited: false, - reblogged: false, - muted: false, - sensitive: false, - spoiler_text: '', - visibility: 'public' as const, - media_attachments: [], - mentions: [], - tags: [], - card: null, - poll: null, - application: null, - language: null, - pinned: false, - emoji_reactions: [], - bookmarked: false, - quote: false, - } -} +export function statusModel( + id: string | null, + acctId: string | null, + emojis: MastodonEntity.Emoji[], + content: string, +) { + const now = "1970-01-02T00:00:00.000Z"; + return { + id: "9atm5frjhb", + uri: "https://http.cat/404", // "" + url: "https://http.cat/404", // "", + account: { + id: "9arzuvv0sw", + username: "ReactionBot", + acct: "ReactionBot", + display_name: "ReactionOfThisPost", + locked: false, + created_at: now, + followers_count: 0, + following_count: 0, + statuses_count: 0, + note: "", + url: "https://http.cat/404", + avatar: "https://http.cat/404", + avatar_static: "https://http.cat/404", + header: "https://http.cat/404", // "" + header_static: "https://http.cat/404", // "" + emojis: [], + fields: [], + moved: null, + bot: false, + }, + in_reply_to_id: id, + in_reply_to_account_id: acctId, + reblog: null, + content: `

${content}

`, + plain_content: null, + created_at: now, + emojis: emojis, + replies_count: 0, + reblogs_count: 0, + favourites_count: 0, + favourited: false, + reblogged: false, + muted: false, + sensitive: false, + spoiler_text: "", + visibility: "public" as const, + media_attachments: [], + mentions: [], + tags: [], + card: null, + poll: null, + application: null, + language: null, + pinned: false, + emoji_reactions: [], + bookmarked: false, + quote: false, + }; +} diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index 3fdb6ce88..9caf43114 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -1,246 +1,305 @@ import Router from "@koa/router"; -import { koaBody } from 'koa-body'; -import megalodon, { Entity, MegalodonInterface } from '@cutls/megalodon'; -import { getClient } from '../ApiMastodonCompatibleService.js' -import { statusModel } from './status.js'; -import Autolinker from 'autolinker'; +import megalodon, { Entity, MegalodonInterface } from "@cutls/megalodon"; +import { getClient } from "../ApiMastodonCompatibleService.js"; +import { statusModel } from "./status.js"; +import Autolinker from "autolinker"; import { ParsedUrlQuery } from "querystring"; export function toLimitToInt(q: ParsedUrlQuery) { - if (q.limit) if (typeof q.limit === 'string') q.limit = parseInt(q.limit, 10).toString() - return q + if (q.limit) + if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10).toString(); + return q; } export function toTextWithReaction(status: Entity.Status[], host: string) { return status.map((t) => { - if (!t) return statusModel(null, null, [], 'no content') - if (!t.emoji_reactions) return t - if (t.reblog) t.reblog = toTextWithReaction([t.reblog], host)[0] - const reactions = t.emoji_reactions.map((r) => `${r.name.replace('@.', '')} (${r.count}${r.me ? "* " : ''})`); - //t.emojis = getEmoji(t.content, host) - t.content = `

${autoLinker(t.content, host)}

${reactions.join(', ')}

` - return t - }) + if (!t) return statusModel(null, null, [], "no content"); + if (!t.emoji_reactions) return t; + if (t.reblog) t.reblog = toTextWithReaction([t.reblog], host)[0]; + const reactions = t.emoji_reactions.map( + (r) => `${r.name.replace("@.", "")} (${r.count}${r.me ? "* " : ""})`, + ); + //t.emojis = getEmoji(t.content, host) + t.content = `

${autoLinker(t.content, host)}

${reactions.join( + ", ", + )}

`; + return t; + }); } export function autoLinker(input: string, host: string) { return Autolinker.link(input, { - hashtag: 'twitter', - mention: 'twitter', - email: false, - stripPrefix: false, - replaceFn : function (match) { - switch(match.type) { - case 'url': - return true - case 'mention': - console.log("Mention: ", match.getMention()); - console.log("Mention Service Name: ", match.getServiceName()); - return `@${match.getMention()}`; - case 'hashtag': - console.log("Hashtag: ", match.getHashtag()); - return `#${match.getHashtag()}`; - } - return false - } - } ); + hashtag: "twitter", + mention: "twitter", + email: false, + stripPrefix: false, + replaceFn: function (match) { + switch (match.type) { + case "url": + return true; + case "mention": + console.log("Mention: ", match.getMention()); + console.log("Mention Service Name: ", match.getServiceName()); + return `@${match.getMention()}`; + case "hashtag": + console.log("Hashtag: ", match.getHashtag()); + return `#${match.getHashtag()}`; + } + return false; + }, + }); } export function apiTimelineMastodon(router: Router): void { - router.get('/v1/timelines/public', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const query: any = ctx.query - const data = query.local ? await client.getLocalTimeline(toLimitToInt(query)) : await client.getPublicTimeline(toLimitToInt(query)); - ctx.body = toTextWithReaction(data.data, ctx.hostname); - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { hashtag: string } }>('/v1/timelines/tag/:hashtag', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getTagTimeline(ctx.params.hashtag, toLimitToInt(ctx.query)); - ctx.body = toTextWithReaction(data.data, ctx.hostname); - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { hashtag: string } }>('/v1/timelines/home', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getHomeTimeline(toLimitToInt(ctx.query)); - ctx.body = toTextWithReaction(data.data, ctx.hostname); - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { listId: string } }>('/v1/timelines/list/:listId', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getListTimeline(ctx.params.listId, toLimitToInt(ctx.query)); - ctx.body = toTextWithReaction(data.data, ctx.hostname); - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.get('/v1/conversations', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getConversationTimeline(toLimitToInt(ctx.query)); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.get('/v1/lists', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getLists(); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>('/v1/lists/:id', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getList(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.post('/v1/lists', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.createList((ctx.query as any).title); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.put<{ Params: { id: string } }>('/v1/lists/:id', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.updateList(ctx.params.id, ctx.query as any); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.delete<{ Params: { id: string } }>('/v1/lists/:id', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.deleteList(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountsInList(ctx.params.id, ctx.query as any); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.addAccountsToList(ctx.params.id, (ctx.query as any).account_ids); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); - router.delete<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.deleteAccountsFromList(ctx.params.id, (ctx.query as any).account_ids); - ctx.body = data.data; - } catch (e: any) { - console.error(e) - console.error(e.response.data) - ctx.status = (401); - ctx.body = e.response.data; - } - }); + router.get("/v1/timelines/public", async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const query: any = ctx.query; + const data = query.local + ? await client.getLocalTimeline(toLimitToInt(query)) + : await client.getPublicTimeline(toLimitToInt(query)); + ctx.body = toTextWithReaction(data.data, ctx.hostname); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get<{ Params: { hashtag: string } }>( + "/v1/timelines/tag/:hashtag", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getTagTimeline( + ctx.params.hashtag, + toLimitToInt(ctx.query), + ); + ctx.body = toTextWithReaction(data.data, ctx.hostname); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { hashtag: string } }>( + "/v1/timelines/home", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getHomeTimeline(toLimitToInt(ctx.query)); + ctx.body = toTextWithReaction(data.data, ctx.hostname); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { listId: string } }>( + "/v1/timelines/list/:listId", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getListTimeline( + ctx.params.listId, + toLimitToInt(ctx.query), + ); + ctx.body = toTextWithReaction(data.data, ctx.hostname); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get("/v1/conversations", async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getConversationTimeline( + toLimitToInt(ctx.query), + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get("/v1/lists", async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getLists(); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get<{ Params: { id: string } }>( + "/v1/lists/:id", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getList(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post("/v1/lists", async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.createList((ctx.query as any).title); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.put<{ Params: { id: string } }>( + "/v1/lists/:id", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.updateList(ctx.params.id, ctx.query as any); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.delete<{ Params: { id: string } }>( + "/v1/lists/:id", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.deleteList(ctx.params.id); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.get<{ Params: { id: string } }>( + "/v1/lists/:id/accounts", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getAccountsInList( + ctx.params.id, + ctx.query as any, + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.post<{ Params: { id: string } }>( + "/v1/lists/:id/accounts", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.addAccountsToList( + ctx.params.id, + (ctx.query as any).account_ids, + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); + router.delete<{ Params: { id: string } }>( + "/v1/lists/:id/accounts", + async (ctx, reply) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.deleteAccountsFromList( + ctx.params.id, + (ctx.query as any).account_ids, + ); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); } function escapeHTML(str: string) { - if (!str) { - return '' - } - return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''') + if (!str) { + return ""; + } + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); } function nl2br(str: string) { - if (!str) { - return '' - } - str = str.replace(/\r\n/g, '
') - str = str.replace(/(\n|\r)/g, '
') - return str + if (!str) { + return ""; + } + str = str.replace(/\r\n/g, "
"); + str = str.replace(/(\n|\r)/g, "
"); + return str; } diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index 5be8ab109..167aa614c 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -152,7 +152,7 @@ export default class Connection { } catch (e) { return; } - + const simpleObj = objs[0]; if (simpleObj.stream) { // is Mastodon Compatible diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts index 4ccad96e8..14e07b748 100644 --- a/packages/backend/src/server/api/streaming.ts +++ b/packages/backend/src/server/api/streaming.ts @@ -16,7 +16,7 @@ export const initializeStreamingServer = (server: http.Server) => { ws.on("request", async (request) => { const q = request.resourceURL.query as ParsedUrlQuery; - const headers = request.httpRequest.headers['sec-websocket-protocol'] || ''; + const headers = request.httpRequest.headers["sec-websocket-protocol"] || ""; const cred = q.i || q.access_token || headers; const accessToken = cred.toString(); @@ -48,9 +48,17 @@ export const initializeStreamingServer = (server: http.Server) => { redisClient.on("message", onRedisMessage); const host = `https://${request.host}`; const prepareStream = q.stream?.toString(); - console.log('start', q); + console.log("start", q); - const main = new MainStreamConnection(connection, ev, user, app, host, accessToken, prepareStream); + const main = new MainStreamConnection( + connection, + ev, + user, + app, + host, + accessToken, + prepareStream, + ); const intervalId = user ? setInterval(() => { diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index 6ade50d18..cd495971e 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -20,8 +20,7 @@ import { createTemp } from "@/misc/create-temp.js"; import { publishMainStream } from "@/services/stream.js"; import * as Acct from "@/misc/acct.js"; import { envOption } from "@/env.js"; -import { koaBody } from 'koa-body'; -import megalodon, { MegalodonInterface } from '@cutls/megalodon'; +import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import activityPub from "./activitypub.js"; import nodeinfo from "./nodeinfo.js"; import wellKnown from "./well-known.js"; @@ -30,6 +29,7 @@ import fileServer from "./file/index.js"; import proxyServer from "./proxy/index.js"; import webServer from "./web/index.js"; import { initializeStreamingServer } from "./api/streaming.js"; +import { koaBody } from "koa-body"; export const serverLogger = new Logger("server", "gray", false); @@ -70,6 +70,11 @@ app.use(mount("/proxy", proxyServer)); // Init router const router = new Router(); +const mastoRouter = new Router(); + +mastoRouter.use(koaBody({ + urlencoded: true +})); // Routing router.use(activityPub.routes()); @@ -135,26 +140,42 @@ router.get("/verify-email/:code", async (ctx) => { } }); -router.get("/oauth/authorize", async (ctx) => { +mastoRouter.get("/oauth/authorize", async (ctx) => { const client_id = ctx.request.query.client_id; console.log(ctx.request.req); - ctx.redirect(Buffer.from(client_id?.toString() || '', 'base64').toString()); + ctx.redirect(Buffer.from(client_id?.toString() || "", "base64").toString()); }); -router.post("/oauth/token", async (ctx) => { +mastoRouter.post("/oauth/token", async (ctx) => { const body: any = ctx.request.body; + let client_id: any = ctx.request.query.client_id; const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const generator = (megalodon as any).default; - const client = generator('misskey', BASE_URL, null) as MegalodonInterface; - const m = body.code.match(/^[a-zA-Z0-9-]+/); - if (!m.length) return { error: 'Invalid code' } + const client = generator("misskey", BASE_URL, null) as MegalodonInterface; + let m = null; + if (body.code) { + m = body.code.match(/^[a-zA-Z0-9-]+/); + if (!m.length) { + ctx.body = { error: "Invalid code" }; + return; + } + } + if (client_id instanceof Array) { + client_id = client_id.toString();; + } else if (!client_id) { + client_id = null; + } try { - const atData = await client.fetchAccessToken(null, body.client_secret, m[0]); + const atData = await client.fetchAccessToken( + client_id, + body.client_secret, + m ? m[0] : '', + ); ctx.body = { access_token: atData.accessToken, - token_type: 'Bearer', - scope: 'read write follow', - created_at: new Date().getTime() / 1000 + token_type: "Bearer", + scope: "read write follow", + created_at: Math.floor(new Date().getTime() / 1000), }; } catch (err: any) { console.error(err); @@ -164,6 +185,7 @@ router.post("/oauth/token", async (ctx) => { }); // Register router +app.use(mastoRouter.routes()); app.use(router.routes()); app.use(mount(webServer)); diff --git a/packages/backend/src/server/web/url-preview.ts b/packages/backend/src/server/web/url-preview.ts index d7da4e72c..c9f3b6cac 100644 --- a/packages/backend/src/server/web/url-preview.ts +++ b/packages/backend/src/server/web/url-preview.ts @@ -44,6 +44,23 @@ export const urlPreviewHandler = async (ctx: Koa.Context) => { logger.succ(`Got preview of ${url}: ${summary.title}`); + if ( + summary.url && + !(summary.url.startsWith("http://") || summary.url.startsWith("https://")) + ) { + throw new Error("unsupported schema included"); + } + + if ( + summary.player?.url && + !( + summary.player.url.startsWith("http://") || + summary.player.url.startsWith("https://") + ) + ) { + throw new Error("unsupported schema included"); + } + summary.icon = wrap(summary.icon); summary.thumbnail = wrap(summary.thumbnail); diff --git a/packages/client/assets/dummy.png b/packages/client/assets/dummy.png new file mode 100644 index 000000000..ee22bdb3c Binary files /dev/null and b/packages/client/assets/dummy.png differ diff --git a/packages/client/assets/dummy_original.png b/packages/client/assets/dummy_original.png new file mode 100644 index 000000000..55c1c595d Binary files /dev/null and b/packages/client/assets/dummy_original.png differ diff --git a/packages/client/src/components/MkUrlPreview.vue b/packages/client/src/components/MkUrlPreview.vue index ef65cb796..865d4bcbe 100644 --- a/packages/client/src/components/MkUrlPreview.vue +++ b/packages/client/src/components/MkUrlPreview.vue @@ -67,6 +67,7 @@ const embedId = `embed${Math.random().toString().replace(/\D/,'')}`; let tweetHeight = $ref(150); const requestUrl = new URL(props.url); +if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url'); if (requestUrl.hostname === 'twitter.com' || requestUrl.hostname === 'mobile.twitter.com') { const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/); diff --git a/packages/client/src/components/global/MkUrl.vue b/packages/client/src/components/global/MkUrl.vue index 2d328211d..d22c0b299 100644 --- a/packages/client/src/components/global/MkUrl.vue +++ b/packages/client/src/components/global/MkUrl.vue @@ -33,6 +33,7 @@ const props = defineProps<{ const self = props.url.startsWith(local); const url = new URL(props.url); +if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url'); const el = ref(); useTooltip(el, (showing) => { diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts index 3f040541f..ddfcc3de1 100644 --- a/packages/client/src/components/mfm.ts +++ b/packages/client/src/components/mfm.ts @@ -377,12 +377,7 @@ export default defineComponent({ case "quote": { if (!this.nowrap) { - return [ - h( - "blockquote", - genEl(token.children), - ), - ]; + return [h("blockquote", genEl(token.children))]; } else { return [ h( diff --git a/packages/client/src/navbar.ts b/packages/client/src/navbar.ts index 5b116cee1..c8c455650 100644 --- a/packages/client/src/navbar.ts +++ b/packages/client/src/navbar.ts @@ -5,10 +5,10 @@ import * as os from "@/os"; import { i18n } from "@/i18n"; import { ui } from "@/config"; import { unisonReload } from "@/scripts/unison-reload"; -import { defaultStore } from '@/store'; -import { instance } from '@/instance'; -import { host } from '@/config'; -import XTutorial from '@/components/MkTutorialDialog.vue'; +import { defaultStore } from "@/store"; +import { instance } from "@/instance"; +import { host } from "@/config"; +import XTutorial from "@/components/MkTutorialDialog.vue"; export const navbarItemDef = reactive({ notifications: { @@ -152,54 +152,68 @@ export const navbarItemDef = reactive({ title: "help", icon: "ph-question-bold ph-lg", action: (ev) => { - os.popupMenu([{ - text: instance.name ?? host, - type: 'label', - }, { - type: 'link', - text: i18n.ts.instanceInfo, - icon: 'ph-info-bold ph-lg', - to: '/about', - }, { - type: 'link', - text: i18n.ts.aboutMisskey, - icon: 'ph-lightbulb-bold ph-lg', - to: '/about-calckey', - }, { - type: 'link', - text: i18n.ts._apps.apps, - icon: 'ph-device-mobile-bold ph-lg', - to: '/apps', - }, { - type: 'button', - action: async () => { - defaultStore.set('tutorial', 0); - os.popup(XTutorial, {}, {}, 'closed'); + os.popupMenu( + [ + { + text: instance.name ?? host, + type: "label", }, - text: i18n.ts.replayTutorial, - icon: 'ph-circle-wavy-question-bold ph-lg', - }, null, { - type: 'parent', - text: i18n.ts.developer, - icon: 'ph-code-bold ph-lg', - children: [{ - type: 'link', - to: '/api-console', - text: 'API Console', - icon: 'ph-terminal-window-bold ph-lg', - }, { - text: i18n.ts.document, - icon: 'ph-file-doc-bold ph-lg', - action: () => { - window.open('/api-doc', '_blank'); + { + type: "link", + text: i18n.ts.instanceInfo, + icon: "ph-info-bold ph-lg", + to: "/about", + }, + { + type: "link", + text: i18n.ts.aboutMisskey, + icon: "ph-lightbulb-bold ph-lg", + to: "/about-calckey", + }, + { + type: "link", + text: i18n.ts._apps.apps, + icon: "ph-device-mobile-bold ph-lg", + to: "/apps", + }, + { + type: "button", + action: async () => { + defaultStore.set("tutorial", 0); + os.popup(XTutorial, {}, {}, "closed"); }, - }, { - type: 'link', - to: '/scratchpad', - text: 'AiScript Scratchpad', - icon: 'ph-scribble-loop-bold ph-lg', - }] - }], ev.currentTarget ?? ev.target, + text: i18n.ts.replayTutorial, + icon: "ph-circle-wavy-question-bold ph-lg", + }, + null, + { + type: "parent", + text: i18n.ts.developer, + icon: "ph-code-bold ph-lg", + children: [ + { + type: "link", + to: "/api-console", + text: "API Console", + icon: "ph-terminal-window-bold ph-lg", + }, + { + text: i18n.ts.document, + icon: "ph-file-doc-bold ph-lg", + action: () => { + window.open("/api-doc", "_blank"); + }, + }, + { + type: "link", + to: "/scratchpad", + text: "AiScript Scratchpad", + icon: "ph-scribble-loop-bold ph-lg", + }, + ], + }, + ], + ev.currentTarget ?? ev.target, ); }, }, diff --git a/packages/client/src/pages/auth.vue b/packages/client/src/pages/auth.vue index bb3c54bd3..9fc04d4f4 100644 --- a/packages/client/src/pages/auth.vue +++ b/packages/client/src/pages/auth.vue @@ -80,6 +80,8 @@ export default defineComponent({ this.state = 'accepted'; const getUrlParams = () => window.location.search.substring(1).split('&').reduce((result, query) => { const [k, v] = query.split('='); result[k] = decodeURI(v); return result; }, {}); if (this.session.app.callbackUrl) { + const url = new URL(this.session.app.callbackUrl); + if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url'); location.href = `${this.session.app.callbackUrl}?token=${this.session.token}&code=${this.session.token}&state=${getUrlParams().state || ''}`; } }, onLogin(res) { diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue index 27f08bdb5..59547a3cb 100644 --- a/packages/client/src/pages/instance-info.vue +++ b/packages/client/src/pages/instance-info.vue @@ -13,7 +13,7 @@
- + {{ instance.name || `(${i18n.ts.unknown})` }}
@@ -156,6 +156,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkPagination from '@/components/MkPagination.vue'; import 'swiper/scss'; import 'swiper/scss/virtual'; +import { getProxiedImageUrlNullable } from '@/scripts/media-proxy'; const props = defineProps<{ host: string; @@ -171,6 +172,7 @@ let meta = $ref(null); let instance = $ref(null); let suspended = $ref(false); let isBlocked = $ref(false); +let faviconUrl = $ref(null); const usersPagination = { endpoint: iAmModerator ? 'admin/show-users' : 'users' as const, @@ -189,6 +191,7 @@ async function fetch() { }); suspended = instance.isSuspended; isBlocked = instance.isBlocked; + faviconUrl = getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.iconUrl, 'preview'); } async function toggleBlock(ev) { diff --git a/packages/client/src/pages/miauth.vue b/packages/client/src/pages/miauth.vue index 6352dc329..a71c7b9a5 100644 --- a/packages/client/src/pages/miauth.vue +++ b/packages/client/src/pages/miauth.vue @@ -70,6 +70,8 @@ async function accept(): Promise { state = 'accepted'; if (props.callback) { + const cbUrl = new URL(props.callback); + if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url'); location.href = appendQuery(props.callback, query({ session: props.session, })); diff --git a/packages/client/src/scripts/aiscript/api.ts b/packages/client/src/scripts/aiscript/api.ts index b37eca8ab..9ec2db963 100644 --- a/packages/client/src/scripts/aiscript/api.ts +++ b/packages/client/src/scripts/aiscript/api.ts @@ -24,7 +24,11 @@ export function createAiScriptEnv(opts) { return confirm.canceled ? values.FALSE : values.TRUE; }), "Mk:api": values.FN_NATIVE(async ([ep, param, token]) => { - if (token) utils.assertString(token); + if (token) { + utils.assertString(token); + // バグがあればundefinedもあり得るため念のため + if (typeof token.value !== "string") throw new Error("invalid token"); + } apiRequests++; if (apiRequests > 16) return values.NULL; const res = await os.api( diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts index bca3b198a..3ce1a8173 100644 --- a/packages/client/src/scripts/get-user-menu.ts +++ b/packages/client/src/scripts/get-user-menu.ts @@ -125,19 +125,22 @@ export function getUserMenu(user, router: Router = mainRouter) { ) return; - await os.apiWithDialog(user.isBlocking ? "blocking/delete" : "blocking/create", { - userId: user.id, - }) + await os.apiWithDialog( + user.isBlocking ? "blocking/delete" : "blocking/create", + { + userId: user.id, + }, + ); user.isBlocking = !user.isBlocking; await os.api(user.isBlocking ? "mute/create" : "mute/delete", { userId: user.id, - }) + }); user.isMuted = user.isBlocking; if (user.isBlocking) { - await os.api('following/delete', { + await os.api("following/delete", { userId: user.id, }); - user.isFollowing = false + user.isFollowing = false; } } diff --git a/packages/client/src/ui/_common_/statusbar-federation.vue b/packages/client/src/ui/_common_/statusbar-federation.vue index f29c6a9ff..2896cc09c 100644 --- a/packages/client/src/ui/_common_/statusbar-federation.vue +++ b/packages/client/src/ui/_common_/statusbar-federation.vue @@ -4,7 +4,7 @@ - + {{ instance.host }} @@ -27,6 +27,7 @@ import * as os from '@/os'; import { useInterval } from '@/scripts/use-interval'; import { getNoteSummary } from '@/scripts/get-note-summary'; import { notePage } from '@/filters/note'; +import { getProxiedImageUrlNullable } from '@/scripts/media-proxy'; const props = defineProps<{ display?: 'marquee' | 'oneByOne'; @@ -56,6 +57,10 @@ useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), { immediate: true, afterMounted: true, }); + +function getInstanceIcon(instance): string { + return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png'; +}