From c68c01a09ebe6ffc656449be4f1de9194e005f09 Mon Sep 17 00:00:00 2001 From: cutestnekoaqua Date: Tue, 7 Feb 2023 22:56:07 +0100 Subject: [PATCH] wip masto api co-authored-by: cutls --- package.json | 1 + packages/backend/package.json | 1 + packages/backend/src/server/api/endpoints.ts | 12 +- packages/backend/src/server/api/index.ts | 3 + .../mastodon/ApiMastodonCompatibleService.ts | 111 ++++++++++++++++++ packages/backend/src/server/index.ts | 21 ++++ packages/client/src/pages/auth.vue | 2 +- pnpm-lock.yaml | 80 ++++++++++++- 8 files changed, 219 insertions(+), 12 deletions(-) create mode 100644 packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts diff --git a/package.json b/package.json index def7df635..8f6cd272d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "gulp": "gulp build", "watch": "pnpm run dev", "dev": "pnpm node ./scripts/dev.js", + "dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start", "lint": "pnpm -r run lint", "cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts", "cy:run": "cypress run", diff --git a/packages/backend/package.json b/packages/backend/package.json index d4b9c1903..1491d6259 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -81,6 +81,7 @@ "koa-send": "5.0.1", "koa-slow": "2.1.0", "koa-views": "7.0.2", + "@cutls/megalodon": "^5.1.1", "mfm-js": "0.23.2", "mime-types": "2.1.35", "mocha": "10.2.0", diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index b6d9c3e1f..69298e73f 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -765,17 +765,17 @@ export interface IEndpointMeta { } export interface IEndpoint { - name: string; - exec: any; - meta: IEndpointMeta; - params: Schema; + name: string, + exec: any, // TODO: may be obosolete @ThatOneCalculator + meta: IEndpointMeta, + params: Schema, } -const endpoints: IEndpoint[] = eps.map(([name, ep]) => { +const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => { return { name: name, exec: ep.default, - meta: ep.meta || {}, + meta: ep.meta ?? {}, params: ep.paramDef, }; }); diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts index b84bbdbb3..da98a9df1 100644 --- a/packages/backend/src/server/api/index.ts +++ b/packages/backend/src/server/api/index.ts @@ -7,6 +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 { Instances, AccessTokens, Users } from "@/models/index.js"; import config from "@/config/index.js"; import endpoints from "./endpoints.js"; @@ -57,6 +58,8 @@ const upload = multer({ // Init router const router = new Router(); +apiMastodonCompatible(router); + /** * Register endpoint handlers */ diff --git a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts new file mode 100644 index 000000000..d389592a1 --- /dev/null +++ b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts @@ -0,0 +1,111 @@ +import Router from "@koa/router"; +import megalodon, { MegalodonInterface } from 'megalodon'; + +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 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' +] +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' +] +export function apiMastodonCompatible(router: Router): void { + + 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.req; + try { + let scope = body.scopes + if (typeof scope === 'string') scope = scope.split(' ') + const pushScope: string[] = [] + for (const s of scope) { + if (s.match(/^read/)) for (const r of readScope) pushScope.push(r) + if (s.match(/^write/)) for (const r of writeScope) pushScope.push(r) + } + 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: pushScope, redirect_uris: red, website: body.website }); + ctx.body = { + id: appData.id, + name: appData.name, + website: appData.website, + redirect_uri: appData.redirectUri, + client_id: Buffer.from(appData.url || '').toString('base64'), + client_secret: appData.clientSecret, + } + } catch (e: any) { + console.error(e) + ctx.status = 401; + ctx.body = e.response.data; + } + }); + + router.get('/v1/accounts/verify_credentials', async (ctx) => { + const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; + const accessTokens = ctx.request.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.verifyAccountCredentials(); + ctx.body = data.data; + } catch (e: any) { + console.error(e) + reply.code(401); + return e.response.data; + } + }); + + + router.get('/v1/custom_emojis', async (ctx) => { + const BASE_URL = request.protocol + '://' + request.hostname; + const accessTokens = request.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getInstanceCustomEmojis(); + return data.data; + } catch (e: any) { + console.error(e) + reply.code(401); + return e.response.data; + } + }); + +} diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index 4d4b81d7a..6b752676f 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -20,6 +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 megalodon, { MegalodonInterface } from 'megalodon'; import activityPub from "./activitypub.js"; import nodeinfo from "./nodeinfo.js"; import wellKnown from "./well-known.js"; @@ -133,6 +134,26 @@ router.get("/verify-email/:code", async (ctx) => { } }); +router.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()); +}); + +router.get("/oauth/token", async (ctx) => { + const body: any = ctx.request.body + const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; + const generator = (megalodon as any).default; + const client = generator('misskey', BASE_URL, null) as MegalodonInterface; + try { + ctx.body = await client.fetchAccessToken(null, body.client_secret, body.code); + } catch (err: any) { + console.error(err); + ctx.status = 401; + ctx.body = err.response.data; + } +}); + // Register router app.use(router.routes()); diff --git a/packages/client/src/pages/auth.vue b/packages/client/src/pages/auth.vue index 22d73a609..81a387422 100644 --- a/packages/client/src/pages/auth.vue +++ b/packages/client/src/pages/auth.vue @@ -79,7 +79,7 @@ export default defineComponent({ accepted() { this.state = 'accepted'; if (this.session.app.callbackUrl) { - location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`; + location.href = `${this.session.app.callbackUrl}?token=${this.session.token}&code=${this.session.token}`; } }, onLogin(res) { login(res.i); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34a4ffc06..f1ce65483 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,6 +163,7 @@ importers: koa-send: 5.0.1 koa-slow: 2.1.0 koa-views: 7.0.2 + megalodon: ^5.1.1 mfm-js: 0.23.2 mime-types: 2.1.35 mocha: 10.2.0 @@ -278,6 +279,7 @@ importers: koa-send: 5.0.1 koa-slow: 2.1.0 koa-views: 7.0.2_6tybghmia4wsnt33xeid7y4rby + megalodon: 5.1.1 mfm-js: 0.23.2 mime-types: 2.1.35 mocha: 10.2.0 @@ -2348,7 +2350,6 @@ packages: resolution: {integrity: sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==} dependencies: '@types/node': 18.11.18 - dev: true /@types/offscreencanvas/2019.3.0: resolution: {integrity: sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==} @@ -2529,6 +2530,12 @@ packages: '@types/node': 18.11.18 dev: true + /@types/ws/8.5.4: + resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} + dependencies: + '@types/node': 18.11.18 + dev: false + /@types/yauzl/2.10.0: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true @@ -3246,7 +3253,7 @@ packages: /axios/0.24.0: resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} dependencies: - follow-redirects: 1.15.2 + follow-redirects: 1.15.2_debug@4.3.4 transitivePeerDependencies: - debug dev: false @@ -3254,11 +3261,21 @@ packages: /axios/0.25.0_debug@4.3.4: resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==} dependencies: - follow-redirects: 1.15.2 + follow-redirects: 1.15.2_debug@4.3.4 transitivePeerDependencies: - debug dev: true + /axios/1.2.2: + resolution: {integrity: sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==} + dependencies: + follow-redirects: 1.15.2_debug@4.3.4 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /babel-eslint/10.1.0_eslint@8.31.0: resolution: {integrity: sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==} engines: {node: '>=6'} @@ -4940,7 +4957,6 @@ packages: /dayjs/1.11.7: resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==} - dev: true /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} @@ -6197,7 +6213,7 @@ packages: readable-stream: 2.3.7 dev: false - /follow-redirects/1.15.2: + /follow-redirects/1.15.2_debug@4.3.4: resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} peerDependencies: @@ -6205,6 +6221,8 @@ packages: peerDependenciesMeta: debug: optional: true + dependencies: + debug: 4.3.4 /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -8654,6 +8672,30 @@ packages: engines: {node: '>= 0.6'} dev: false + /megalodon/5.1.1: + resolution: {integrity: sha512-zsYzzmogmk9lnXzGk3kKv58LUmZVFMebiya/1CZqZYnBVxq18Ep8l1AU41o+INANMqYxG+hAQvJhE+Z5dcUabQ==} + engines: {node: '>=15.0.0'} + dependencies: + '@types/oauth': 0.9.1 + '@types/ws': 8.5.4 + axios: 1.2.2 + dayjs: 1.11.7 + form-data: 4.0.0 + https-proxy-agent: 5.0.1 + oauth: 0.10.0 + object-assign-deep: 0.4.0 + parse-link-header: 2.0.0 + socks-proxy-agent: 7.0.0 + typescript: 4.9.4 + uuid: 9.0.0 + ws: 8.12.0 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + dev: false + /merge-stream/2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -9321,6 +9363,11 @@ packages: resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} dev: false + /object-assign-deep/0.4.0: + resolution: {integrity: sha512-54Uvn3s+4A/cMWx9tlRez1qtc7pN7pbQ+Yi7mjLjcBpWLlP+XbSHiHbQW6CElDiV4OvuzqnMrBdkgxI1mT8V/Q==} + engines: {node: '>=6'} + dev: false + /object-assign/4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -9613,6 +9660,12 @@ packages: error-ex: 1.3.2 dev: false + /parse-link-header/2.0.0: + resolution: {integrity: sha512-xjU87V0VyHZybn2RrCX5TIFGxTVZE6zqqZWMPlIKiSKuWh/X5WZdt+w1Ki1nXB+8L/KtL+nZ4iq+sfI6MrhhMw==} + dependencies: + xtend: 4.0.2 + dev: false + /parse-node-version/1.0.1: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} @@ -10254,6 +10307,10 @@ packages: resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} dev: true + /proxy-from-env/1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /ps-tree/1.2.0: resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} engines: {node: '>= 0.10'} @@ -12943,6 +13000,19 @@ packages: optional: true dev: false + /ws/8.12.0: + resolution: {integrity: sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xev/3.0.2: resolution: {integrity: sha512-8kxuH95iMXzHZj+fwqfA4UrPcYOy6bGIgfWzo9Ji23JoEc30ge/Z++Ubkiuy8c0+M64nXmmxrmJ7C8wnuBhluw==} dev: false