diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts index fe0b6a4d7..64412ac90 100644 --- a/packages/backend/src/server/api/index.ts +++ b/packages/backend/src/server/api/index.ts @@ -7,13 +7,9 @@ import Router from "@koa/router"; import multer from "@koa/multer"; import bodyParser from "koa-bodyparser"; import cors from "@koa/cors"; -import { - apiMastodonCompatible, - getClient, -} from "./mastodon/ApiMastodonCompatibleService.js"; -import { Instances, AccessTokens, Users } from "@/models/index.js"; +import { setupMastodonApi } from "./mastodon/index.js"; +import { AccessTokens, Users } from "@/models/index.js"; import config from "@/config/index.js"; -import fs from "fs"; import endpoints from "./endpoints.js"; import compatibility from "./compatibility.js"; import handler from "./api-handler.js"; @@ -24,9 +20,7 @@ import verifyEmail from "./private/verify-email.js"; import discord from "./service/discord.js"; import github from "./service/github.js"; import twitter from "./service/twitter.js"; -import { koaBody } from "koa-body"; import { convertId, IdType } from "@/misc/convert-id.js"; -import { convertAttachment } from "./mastodon/converters.js"; // re-export native rust id conversion (function and enum) export { IdType, convertId }; @@ -72,64 +66,7 @@ router.use( }), ); -mastoRouter.use( - koaBody({ - multipart: true, - urlencoded: true, - }), -); - -mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const multipartData = await ctx.file; - if (!multipartData) { - ctx.body = { error: "No image" }; - ctx.status = 401; - return; - } - const data = await client.uploadMedia(multipartData); - ctx.body = convertAttachment(data.data as MastodonEntity.Attachment); - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } -}); -mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const multipartData = await ctx.file; - if (!multipartData) { - ctx.body = { error: "No image" }; - ctx.status = 401; - return; - } - const data = await client.uploadMedia(multipartData, ctx.request.body); - ctx.body = convertAttachment(data.data as MastodonEntity.Attachment); - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } -}); - -mastoRouter.use(async (ctx, next) => { - if (ctx.request.query) { - if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) { - ctx.request.body = ctx.request.query; - } else { - ctx.request.body = { ...ctx.request.body, ...ctx.request.query }; - } - } - await next(); -}); - -apiMastodonCompatible(mastoRouter); +setupMastodonApi(mastoRouter, mastoFileRouter, upload); /** * Register endpoint handlers diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index 3d6e1a981..2175e21ee 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -1,5 +1,5 @@ import Router from "@koa/router"; -import { getClient } from "../ApiMastodonCompatibleService.js"; +import { getClient } from "../index.js"; import { argsToBools, convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "./timeline.js"; import { convertId, IdType } from "../../index.js"; import { convertAccount, convertFeaturedTag, convertList, convertRelationship, convertStatus, } from "../converters.js"; @@ -10,7 +10,7 @@ import { NoteConverter } from "@/server/api/mastodon/converters/note.js"; import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; -export function apiAccountMastodon(router: Router): void { +export function setupEndpointsAccount(router: Router): void { router.get("/v1/accounts/verify_credentials", async (ctx) => { try { const auth = await authenticate(ctx.headers.authorization, null); diff --git a/packages/backend/src/server/api/mastodon/endpoints/auth.ts b/packages/backend/src/server/api/mastodon/endpoints/auth.ts index 14e02e6d9..be31edcc6 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/auth.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/auth.ts @@ -39,7 +39,7 @@ const writeScope = [ "write:gallery-likes", ]; -export function apiAuthMastodon(router: Router): void { +export function setupEndpointsAuth(router: Router): void { router.post("/v1/apps", async (ctx) => { const body: any = ctx.request.body || ctx.request.query; try { diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts index ff30e90be..3659a8bba 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts @@ -1,10 +1,10 @@ import megalodon, { MegalodonInterface } from "megalodon"; import Router from "@koa/router"; -import { getClient } from "../ApiMastodonCompatibleService.js"; +import { getClient } from "../index.js"; import { IdType, convertId } from "../../index.js"; import { convertFilter } from "../converters.js"; -export function apiFilterMastodon(router: Router): void { +export function setupEndpointsFilter(router: Router): void { router.get("/v1/filters", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; diff --git a/packages/backend/src/server/api/mastodon/endpoints/media.ts b/packages/backend/src/server/api/mastodon/endpoints/media.ts new file mode 100644 index 000000000..a6819e825 --- /dev/null +++ b/packages/backend/src/server/api/mastodon/endpoints/media.ts @@ -0,0 +1,78 @@ +import Router from "@koa/router"; +import { getClient } from "@/server/api/mastodon/index.js"; +import { convertId, IdType } from "@/misc/convert-id.js"; +import { convertAttachment } from "@/server/api/mastodon/converters.js"; +import multer from "@koa/multer"; + +export function setupEndpointsMedia(router: Router, fileRouter: Router, upload: multer.Instance): void { + router.get<{ Params: { id: string } }>("/v1/media/:id", 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.getMedia( + convertId(ctx.params.id, IdType.IceshrimpId), + ); + ctx.body = convertAttachment(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) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.updateMedia( + convertId(ctx.params.id, IdType.IceshrimpId), + ctx.request.body as any, + ); + ctx.body = convertAttachment(data.data); + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + + fileRouter.post("/v1/media", upload.single("file"), async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const multipartData = await ctx.file; + if (!multipartData) { + ctx.body = { error: "No image" }; + ctx.status = 401; + return; + } + const data = await client.uploadMedia(multipartData); + ctx.body = convertAttachment(data.data as MastodonEntity.Attachment); + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + fileRouter.post("/v2/media", upload.single("file"), async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const multipartData = await ctx.file; + if (!multipartData) { + ctx.body = { error: "No image" }; + ctx.status = 401; + return; + } + const data = await client.uploadMedia(multipartData, ctx.request.body); + ctx.body = convertAttachment(data.data as MastodonEntity.Attachment); + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }); +} diff --git a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts b/packages/backend/src/server/api/mastodon/endpoints/misc.ts similarity index 76% rename from packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts rename to packages/backend/src/server/api/mastodon/endpoints/misc.ts index 1dc7ca4cc..694c60771 100644 --- a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/misc.ts @@ -1,42 +1,17 @@ import Router from "@koa/router"; -import megalodon, { MegalodonInterface } from "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 { getClient } from "@/server/api/mastodon/index.js"; +import { convertId, IdType } from "@/misc/convert-id.js"; import { convertAccount, convertAnnouncement, - convertFilter, -} from "./converters.js"; -import { convertId, IdType } from "../index.js"; + convertAttachment, + convertFilter +} from "@/server/api/mastodon/converters.js"; import { Users } from "@/models/index.js"; +import { getInstance } from "@/server/api/mastodon/endpoints/meta.js"; import { IsNull } from "typeorm"; -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(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); - +export function setupEndpointsMisc(router: Router): void { router.get("/v1/custom_emojis", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts index 91c73d374..2281efa1f 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts @@ -7,7 +7,7 @@ import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import { NotificationHelpers } from "@/server/api/mastodon/helpers/notification.js"; import { NotificationConverter } from "@/server/api/mastodon/converters/notification.js"; -export function apiNotificationsMastodon(router: Router): void { +export function setupEndpointsNotifications(router: Router): void { router.get("/v1/notifications", async (ctx) => { try { const auth = await authenticate(ctx.headers.authorization, null); diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts index c08c52b24..5f5a49c56 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/search.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts @@ -1,12 +1,12 @@ import megalodon, { MegalodonInterface } from "megalodon"; import Router from "@koa/router"; -import { getClient } from "../ApiMastodonCompatibleService.js"; +import { getClient } from "../index.js"; import axios from "axios"; import { Converter } from "megalodon"; import { convertPaginationArgsIds, limitToInt } from "./timeline.js"; import { convertAccount, convertStatus } from "../converters.js"; -export function apiSearchMastodon(router: Router): void { +export function setupEndpointsSearch(router: Router): void { router.get("/v1/search", async (ctx) => { const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const accessTokens = ctx.request.headers.authorization; diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index dab1648d0..02f6fc1c4 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -1,5 +1,5 @@ import Router from "@koa/router"; -import { getClient } from "../ApiMastodonCompatibleService.js"; +import { getClient } from "../index.js"; import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js"; import querystring from "node:querystring"; import qs from "qs"; @@ -19,7 +19,7 @@ function normalizeQuery(data: any) { return qs.parse(str); } -export function apiStatusMastodon(router: Router): void { +export function setupEndpointsStatus(router: Router): void { router.post("/v1/statuses", async (ctx) => { const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const accessTokens = ctx.headers.authorization; @@ -643,38 +643,6 @@ export function apiStatusMastodon(router: Router): void { } }, ); - - router.get<{ Params: { id: string } }>("/v1/media/:id", 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.getMedia( - convertId(ctx.params.id, IdType.IceshrimpId), - ); - ctx.body = convertAttachment(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) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.updateMedia( - convertId(ctx.params.id, IdType.IceshrimpId), - ctx.request.body as any, - ); - ctx.body = convertAttachment(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) => { const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const accessTokens = ctx.headers.authorization; diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index 82b8c8ed5..d102e6534 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -1,5 +1,5 @@ import Router from "@koa/router"; -import { getClient } from "../ApiMastodonCompatibleService.js"; +import { getClient } from "../index.js"; import { ParsedUrlQuery } from "querystring"; import { convertAccount, @@ -66,7 +66,7 @@ export function normalizeUrlQuery(q: ParsedUrlQuery, arrayKeys: string[] = []): return dict; } -export function apiTimelineMastodon(router: Router): void { +export function setupEndpointsTimeline(router: Router): void { router.get("/v1/timelines/public", async (ctx, reply) => { try { const auth = await authenticate(ctx.headers.authorization, null); diff --git a/packages/backend/src/server/api/mastodon/index.ts b/packages/backend/src/server/api/mastodon/index.ts new file mode 100644 index 000000000..069fbf9e4 --- /dev/null +++ b/packages/backend/src/server/api/mastodon/index.ts @@ -0,0 +1,54 @@ +import Router from "@koa/router"; +import megalodon, { MegalodonInterface } from "megalodon"; +import { setupEndpointsAuth } from "./endpoints/auth.js"; +import { setupEndpointsAccount } from "./endpoints/account.js"; +import { setupEndpointsStatus } from "./endpoints/status.js"; +import { setupEndpointsFilter } from "./endpoints/filter.js"; +import { setupEndpointsTimeline } from "./endpoints/timeline.js"; +import { setupEndpointsNotifications } from "./endpoints/notifications.js"; +import { setupEndpointsSearch } from "./endpoints/search.js"; +import { setupEndpointsMedia } from "@/server/api/mastodon/endpoints/media.js"; +import { setupEndpointsMisc } from "@/server/api/mastodon/endpoints/misc.js"; +import { koaBody } from "koa-body"; +import multer from "@koa/multer"; + +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(BASE_URL, accessToken) as MegalodonInterface; + return client; +} + +export function setupMastodonApi(router: Router, fileRouter: Router, upload: multer.Instance): void { + router.use( + koaBody({ + multipart: true, + urlencoded: true, + }), + ); + + router.use(async (ctx, next) => { + if (ctx.request.query) { + if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) { + ctx.request.body = ctx.request.query; + } else { + ctx.request.body = { ...ctx.request.body, ...ctx.request.query }; + } + } + await next(); + }); + + setupEndpointsAuth(router); + setupEndpointsAccount(router); + setupEndpointsStatus(router); + setupEndpointsFilter(router); + setupEndpointsTimeline(router); + setupEndpointsNotifications(router); + setupEndpointsSearch(router); + setupEndpointsMedia(router, fileRouter, upload); + setupEndpointsMisc(router); +} diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index 72dce1951..253b5bdc0 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -26,7 +26,7 @@ import channels from "./channels/index.js"; import type Channel from "./channel.js"; import type { StreamEventEmitter, StreamMessages } from "./types.js"; import { Converter } from "megalodon"; -import { getClient } from "../mastodon/ApiMastodonCompatibleService.js"; +import { getClient } from "../mastodon/index.js"; /** * Main stream connection