mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-14 14:07:38 -07:00
feat: ✨ v1 Mastodon API
This commit adds (maybe unstable) support for Mastodons v1 api also some v2 endpoints, maybe I miss stuff, I dont know. We will need to test this but it should be kinda stable and work like (old) butter. Co-authored-by: Natty <natty.sh.git@gmail.com> Co-authored-by: cutls <web-pro@cutls.com>
This commit is contained in:
parent
bb711adff8
commit
b1e3c2e1c7
21 changed files with 1805 additions and 198 deletions
|
@ -40,7 +40,10 @@
|
||||||
"@tensorflow/tfjs": "^4.2.0",
|
"@tensorflow/tfjs": "^4.2.0",
|
||||||
"ajv": "8.11.2",
|
"ajv": "8.11.2",
|
||||||
"archiver": "5.3.1",
|
"archiver": "5.3.1",
|
||||||
|
"koa-body": "^6.0.1",
|
||||||
"autobind-decorator": "2.4.0",
|
"autobind-decorator": "2.4.0",
|
||||||
|
"autolinker": "4.0.0",
|
||||||
|
"axios": "^1.3.2",
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"aws-sdk": "2.1277.0",
|
"aws-sdk": "2.1277.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
|
@ -81,7 +84,7 @@
|
||||||
"koa-send": "5.0.1",
|
"koa-send": "5.0.1",
|
||||||
"koa-slow": "2.1.0",
|
"koa-slow": "2.1.0",
|
||||||
"koa-views": "7.0.2",
|
"koa-views": "7.0.2",
|
||||||
"@cutls/megalodon": "^5.1.1",
|
"@cutls/megalodon": "5.1.15",
|
||||||
"mfm-js": "0.23.2",
|
"mfm-js": "0.23.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"mocha": "10.2.0",
|
"mocha": "10.2.0",
|
||||||
|
|
|
@ -2,3 +2,4 @@ import twemoji from "twemoji-parser/dist/lib/regex.js";
|
||||||
const twemojiRegex = twemoji.default;
|
const twemojiRegex = twemoji.default;
|
||||||
|
|
||||||
export const emojiRegex = new RegExp(`(${twemojiRegex.source})`);
|
export const emojiRegex = new RegExp(`(${twemojiRegex.source})`);
|
||||||
|
export const emojiRegexAtStartToEnd = new RegExp(`^(${twemojiRegex.source})$`);
|
||||||
|
|
|
@ -197,6 +197,8 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||||
.map((x) => decodeReaction(x).reaction)
|
.map((x) => decodeReaction(x).reaction)
|
||||||
.map((x) => x.replace(/:/g, ""));
|
.map((x) => x.replace(/:/g, ""));
|
||||||
|
|
||||||
|
const noteEmoji = await populateEmojis(note.emojis.concat(reactionEmojiNames), host);
|
||||||
|
const reactionEmoji = await populateEmojis(reactionEmojiNames, host);
|
||||||
const packed: Packed<"Note"> = await awaitAll({
|
const packed: Packed<"Note"> = await awaitAll({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
createdAt: note.createdAt.toISOString(),
|
createdAt: note.createdAt.toISOString(),
|
||||||
|
@ -213,8 +215,9 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||||
renoteCount: note.renoteCount,
|
renoteCount: note.renoteCount,
|
||||||
repliesCount: note.repliesCount,
|
repliesCount: note.repliesCount,
|
||||||
reactions: convertLegacyReactions(note.reactions),
|
reactions: convertLegacyReactions(note.reactions),
|
||||||
|
reactionEmojis: reactionEmoji,
|
||||||
|
emojis: noteEmoji,
|
||||||
tags: note.tags.length > 0 ? note.tags : undefined,
|
tags: note.tags.length > 0 ? note.tags : undefined,
|
||||||
emojis: populateEmojis(note.emojis.concat(reactionEmojiNames), host),
|
|
||||||
fileIds: note.fileIds,
|
fileIds: note.fileIds,
|
||||||
files: DriveFiles.packMany(note.fileIds),
|
files: DriveFiles.packMany(note.fileIds),
|
||||||
replyId: note.replyId,
|
replyId: note.replyId,
|
||||||
|
|
|
@ -161,26 +161,8 @@ export const packedNoteSchema = {
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
emojis: {
|
emojis: {
|
||||||
type: "array",
|
type: 'object',
|
||||||
optional: false,
|
optional: true, nullable: true,
|
||||||
nullable: false,
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
optional: false,
|
|
||||||
nullable: false,
|
|
||||||
properties: {
|
|
||||||
name: {
|
|
||||||
type: "string",
|
|
||||||
optional: false,
|
|
||||||
nullable: false,
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
type: "string",
|
|
||||||
optional: false,
|
|
||||||
nullable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
reactions: {
|
reactions: {
|
||||||
type: "object",
|
type: "object",
|
||||||
|
|
|
@ -198,6 +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_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_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_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_get from "./endpoints/i/registry/get.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_keysWithType from "./endpoints/i/registry/keys-with-type.js";
|
||||||
import * as ep___i_registry_keys from "./endpoints/i/registry/keys.js";
|
import * as ep___i_registry_keys from "./endpoints/i/registry/keys.js";
|
||||||
|
@ -538,6 +539,7 @@ const eps = [
|
||||||
["i/regenerate-token", ep___i_regenerateToken],
|
["i/regenerate-token", ep___i_regenerateToken],
|
||||||
["i/registry/get-all", ep___i_registry_getAll],
|
["i/registry/get-all", ep___i_registry_getAll],
|
||||||
["i/registry/get-detail", ep___i_registry_getDetail],
|
["i/registry/get-detail", ep___i_registry_getDetail],
|
||||||
|
["i/registry/get-unsecure", ep___i_registry_getUnsecure],
|
||||||
["i/registry/get", ep___i_registry_get],
|
["i/registry/get", ep___i_registry_get],
|
||||||
["i/registry/keys-with-type", ep___i_registry_keysWithType],
|
["i/registry/keys-with-type", ep___i_registry_keysWithType],
|
||||||
["i/registry/keys", ep___i_registry_keys],
|
["i/registry/keys", ep___i_registry_keys],
|
||||||
|
|
50
packages/backend/src/server/api/endpoints/i/get-unsecure.ts
Normal file
50
packages/backend/src/server/api/endpoints/i/get-unsecure.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { ApiError } from "../../error.js";
|
||||||
|
import define from "../../define.js";
|
||||||
|
import { Items } from "@/";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
secure: false,
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchKey: {
|
||||||
|
message: "No such key.",
|
||||||
|
code: "NO_SUCH_KEY",
|
||||||
|
id: "ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
key: { type: "string" },
|
||||||
|
scope: {
|
||||||
|
type: "array",
|
||||||
|
default: [],
|
||||||
|
items: {
|
||||||
|
type: "string",
|
||||||
|
pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["key"],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
if (ps.key !== "reactions") return;
|
||||||
|
const query = Items.createQueryBuilder("item")
|
||||||
|
.where("item.domain IS NULL")
|
||||||
|
.andWhere("item.userId = :userId", { userId: user.id })
|
||||||
|
.andWhere("item.key = :key", { key: ps.key })
|
||||||
|
.andWhere("item.scope = :scope", { scope: ps.scope });
|
||||||
|
|
||||||
|
const item = await query.getOne();
|
||||||
|
|
||||||
|
if (item == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.value;
|
||||||
|
});
|
|
@ -1,98 +1,30 @@
|
||||||
import Router from "@koa/router";
|
import Router from "@koa/router";
|
||||||
import megalodon, { MegalodonInterface } from 'megalodon';
|
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';
|
||||||
|
|
||||||
function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface {
|
export function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface {
|
||||||
const accessTokenArr = authorization?.split(' ') ?? [null];
|
const accessTokenArr = authorization?.split(' ') ?? [null];
|
||||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||||
const generator = (megalodon as any).default
|
const generator = (megalodon as any).default
|
||||||
const client = generator('misskey', BASE_URL, accessToken) as MegalodonInterface;
|
const client = generator('misskey', BASE_URL, accessToken) as MegalodonInterface;
|
||||||
return client
|
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 {
|
export function apiMastodonCompatible(router: Router): void {
|
||||||
|
apiAuthMastodon(router)
|
||||||
router.post('/v1/apps', async (ctx) => {
|
apiAccountMastodon(router)
|
||||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
apiStatusMastodon(router)
|
||||||
const accessTokens = ctx.request.headers.authorization;
|
apiFilterMastodon(router)
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
apiTimelineMastodon(router)
|
||||||
const body: any = ctx.request.req;
|
apiNotificationsMastodon(router)
|
||||||
try {
|
apiSearchMastodon(router)
|
||||||
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)
|
|
||||||
ctx.status = 401;
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
router.get('/v1/custom_emojis', async (ctx) => {
|
router.get('/v1/custom_emojis', async (ctx) => {
|
||||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||||
|
@ -108,4 +40,19 @@ export function apiMastodonCompatible(router: Router): void {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
try {
|
||||||
|
const data = await client.getInstance();
|
||||||
|
ctx.body = getInstance(data.data);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
323
packages/backend/src/server/api/mastodon/endpoints/account.ts
Normal file
323
packages/backend/src/server/api/mastodon/endpoints/account.ts
Normal file
|
@ -0,0 +1,323 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
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', koaBody(), 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', koaBody(), 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
81
packages/backend/src/server/api/mastodon/endpoints/auth.ts
Normal file
81
packages/backend/src/server/api/mastodon/endpoints/auth.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import megalodon, { MegalodonInterface } from '@cutls/megalodon';
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import { koaBody } from 'koa-body';
|
||||||
|
import { getClient } from '../ApiMastodonCompatibleService.js';
|
||||||
|
|
||||||
|
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 apiAuthMastodon(router: Router): void {
|
||||||
|
|
||||||
|
router.post('/v1/apps', koaBody(), 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<string>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
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_secret: appData.clientSecret,
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
83
packages/backend/src/server/api/mastodon/endpoints/filter.ts
Normal file
83
packages/backend/src/server/api/mastodon/endpoints/filter.ts
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import megalodon, { MegalodonInterface } from '@cutls/megalodon';
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import { koaBody } from 'koa-body';
|
||||||
|
import { getClient } from '../ApiMastodonCompatibleService.js';
|
||||||
|
|
||||||
|
export function apiFilterMastodon(router: Router): void {
|
||||||
|
|
||||||
|
router.get('/v1/filters', koaBody(), 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.getFilters();
|
||||||
|
ctx.body = data.data;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/v1/filters/:id', koaBody(), 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.getFilter(ctx.params.id);
|
||||||
|
ctx.body = data.data;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/v1/filters', koaBody(), 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.createFilter(body.phrase, body.context, body);
|
||||||
|
ctx.body = data.data;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/v1/filters/:id', koaBody(), 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);
|
||||||
|
ctx.body = data.data;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/v1/filters/:id', koaBody(), 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.deleteFilter(ctx.params.id);
|
||||||
|
ctx.body = data.data;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
97
packages/backend/src/server/api/mastodon/endpoints/meta.ts
Normal file
97
packages/backend/src/server/api/mastodon/endpoints/meta.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import { Entity } from "@cutls/megalodon";
|
||||||
|
// TODO: add calckey features
|
||||||
|
export function getInstance(response: Entity.Instance) {
|
||||||
|
return {
|
||||||
|
uri: response.uri,
|
||||||
|
title: response.title || "",
|
||||||
|
short_description: response.description || "",
|
||||||
|
description: response.description || "",
|
||||||
|
email: response.email || "",
|
||||||
|
version: "3.0.0 compatible (Calckey)",
|
||||||
|
urls: response.urls,
|
||||||
|
stats: response.stats,
|
||||||
|
thumbnail: response.thumbnail || "",
|
||||||
|
languages: ["en", "de", "ja"],
|
||||||
|
registrations: response.registrations,
|
||||||
|
approval_required: !response.registrations,
|
||||||
|
invites_enabled: response.registrations,
|
||||||
|
configuration: {
|
||||||
|
accounts: {
|
||||||
|
max_featured_tags: 20,
|
||||||
|
},
|
||||||
|
statuses: {
|
||||||
|
max_characters: 3000,
|
||||||
|
max_media_attachments: 4,
|
||||||
|
characters_reserved_per_url: response.uri.length,
|
||||||
|
},
|
||||||
|
media_attachments: {
|
||||||
|
supported_mime_types: [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"image/heic",
|
||||||
|
"image/heif",
|
||||||
|
"image/webp",
|
||||||
|
"image/avif",
|
||||||
|
"video/webm",
|
||||||
|
"video/mp4",
|
||||||
|
"video/quicktime",
|
||||||
|
"video/ogg",
|
||||||
|
"audio/wave",
|
||||||
|
"audio/wav",
|
||||||
|
"audio/x-wav",
|
||||||
|
"audio/x-pn-wave",
|
||||||
|
"audio/vnd.wave",
|
||||||
|
"audio/ogg",
|
||||||
|
"audio/vorbis",
|
||||||
|
"audio/mpeg",
|
||||||
|
"audio/mp3",
|
||||||
|
"audio/webm",
|
||||||
|
"audio/flac",
|
||||||
|
"audio/aac",
|
||||||
|
"audio/m4a",
|
||||||
|
"audio/x-m4a",
|
||||||
|
"audio/mp4",
|
||||||
|
"audio/3gpp",
|
||||||
|
"video/x-ms-asf",
|
||||||
|
],
|
||||||
|
image_size_limit: 10485760,
|
||||||
|
image_matrix_limit: 16777216,
|
||||||
|
video_size_limit: 41943040,
|
||||||
|
video_frame_rate_limit: 60,
|
||||||
|
video_matrix_limit: 2304000,
|
||||||
|
},
|
||||||
|
polls: {
|
||||||
|
max_options: 8,
|
||||||
|
max_characters_per_option: 50,
|
||||||
|
min_expiration: 300,
|
||||||
|
max_expiration: 2629746,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contact_account: {
|
||||||
|
id: "1",
|
||||||
|
username: "admin",
|
||||||
|
acct: "admin",
|
||||||
|
display_name: "admin",
|
||||||
|
locked: true,
|
||||||
|
bot: true,
|
||||||
|
discoverable: false,
|
||||||
|
group: false,
|
||||||
|
created_at: "1971-01-01T00:00:00.000Z",
|
||||||
|
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",
|
||||||
|
followers_count: -1,
|
||||||
|
following_count: 0,
|
||||||
|
statuses_count: 0,
|
||||||
|
last_status_at: "1971-01-01T00:00:00.000Z",
|
||||||
|
noindex: true,
|
||||||
|
emojis: [],
|
||||||
|
fields: [],
|
||||||
|
},
|
||||||
|
rules: [],
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
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';
|
||||||
|
function toLimitToInt(q: any) {
|
||||||
|
if (q.limit) if (typeof q.limit === 'string') q.limit = parseInt(q.limit, 10)
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apiNotificationMastodon(router: Router): void {
|
||||||
|
|
||||||
|
router.get('/v1/notifications', koaBody(), 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.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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ctx.body = ret;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/v1/notification/:id', koaBody(), 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 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]
|
||||||
|
} else {
|
||||||
|
ctx.body = data
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/v1/notifications/clear', koaBody(), 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.dismissNotifications();
|
||||||
|
ctx.body = data.data;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/v1/notification/:id/dismiss', koaBody(), 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.dismissNotification(ctx.params.id);
|
||||||
|
ctx.body = data.data;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
25
packages/backend/src/server/api/mastodon/endpoints/search.ts
Normal file
25
packages/backend/src/server/api/mastodon/endpoints/search.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import megalodon, { MegalodonInterface } from '@cutls/megalodon';
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import { koaBody } from 'koa-body';
|
||||||
|
import { getClient } from '../ApiMastodonCompatibleService.js';
|
||||||
|
|
||||||
|
export function apiSearchMastodon(router: Router): void {
|
||||||
|
|
||||||
|
router.get('/v1/search', koaBody(), 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 data = await client.search(query.q, type, query);
|
||||||
|
ctx.body = data.data;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
403
packages/backend/src/server/api/mastodon/endpoints/status.ts
Normal file
403
packages/backend/src/server/api/mastodon/endpoints/status.ts
Normal file
|
@ -0,0 +1,403 @@
|
||||||
|
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';
|
||||||
|
const pump = promisify(pipeline);
|
||||||
|
|
||||||
|
export function apiStatusMastodon(router: Router): void {
|
||||||
|
router.post('/v1/statuses', koaBody(), 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('<br />')
|
||||||
|
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/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/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', koaBody(), 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', koaBody(), 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: `<p>${content}</p>`,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
246
packages/backend/src/server/api/mastodon/endpoints/timeline.ts
Normal file
246
packages/backend/src/server/api/mastodon/endpoints/timeline.ts
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
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 { 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `<p>${autoLinker(t.content, host)}</p><p>${reactions.join(', ')}</p>`
|
||||||
|
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 `<a href="https://${host}/@${encodeURIComponent(match.getMention())}" target="_blank">@${match.getMention()}</a>`;
|
||||||
|
case 'hashtag':
|
||||||
|
console.log("Hashtag: ", match.getHashtag());
|
||||||
|
return `<a href="https://${host}/tags/${encodeURIComponent(match.getHashtag())}" target="_blank">#${match.getHashtag()}</a>`;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function escapeHTML(str: string) {
|
||||||
|
if (!str) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
function nl2br(str: string) {
|
||||||
|
if (!str) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
str = str.replace(/\r\n/g, '<br />')
|
||||||
|
str = str.replace(/(\n|\r)/g, '<br />')
|
||||||
|
return str
|
||||||
|
}
|
|
@ -24,6 +24,9 @@ import { readNotification } from "../common/read-notification.js";
|
||||||
import channels from "./channels/index.js";
|
import channels from "./channels/index.js";
|
||||||
import type Channel from "./channel.js";
|
import type Channel from "./channel.js";
|
||||||
import type { StreamEventEmitter, StreamMessages } from "./types.js";
|
import type { StreamEventEmitter, StreamMessages } from "./types.js";
|
||||||
|
import { Converter } from "@cutls/megalodon";
|
||||||
|
import { getClient } from "../mastodon/ApiMastodonCompatibleService.js";
|
||||||
|
import { toTextWithReaction } from "../mastodon/endpoints/timeline.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main stream connection
|
* Main stream connection
|
||||||
|
@ -41,17 +44,27 @@ export default class Connection {
|
||||||
private channels: Channel[] = [];
|
private channels: Channel[] = [];
|
||||||
private subscribingNotes: any = {};
|
private subscribingNotes: any = {};
|
||||||
private cachedNotes: Packed<"Note">[] = [];
|
private cachedNotes: Packed<"Note">[] = [];
|
||||||
|
private isMastodonCompatible: boolean = false;
|
||||||
|
private host: string;
|
||||||
|
private accessToken: string;
|
||||||
|
private currentSubscribe: string[][] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
wsConnection: websocket.connection,
|
wsConnection: websocket.connection,
|
||||||
subscriber: EventEmitter,
|
subscriber: EventEmitter,
|
||||||
user: User | null | undefined,
|
user: User | null | undefined,
|
||||||
token: AccessToken | null | undefined,
|
token: AccessToken | null | undefined,
|
||||||
|
host: string,
|
||||||
|
accessToken: string,
|
||||||
|
prepareStream: string | undefined,
|
||||||
) {
|
) {
|
||||||
|
console.log("constructor", prepareStream);
|
||||||
this.wsConnection = wsConnection;
|
this.wsConnection = wsConnection;
|
||||||
this.subscriber = subscriber;
|
this.subscriber = subscriber;
|
||||||
if (user) this.user = user;
|
if (user) this.user = user;
|
||||||
if (token) this.token = token;
|
if (token) this.token = token;
|
||||||
|
if (host) this.host = host;
|
||||||
|
if (accessToken) this.accessToken = accessToken;
|
||||||
|
|
||||||
this.onWsConnectionMessage = this.onWsConnectionMessage.bind(this);
|
this.onWsConnectionMessage = this.onWsConnectionMessage.bind(this);
|
||||||
this.onUserEvent = this.onUserEvent.bind(this);
|
this.onUserEvent = this.onUserEvent.bind(this);
|
||||||
|
@ -73,6 +86,13 @@ export default class Connection {
|
||||||
|
|
||||||
this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
|
this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
|
||||||
}
|
}
|
||||||
|
console.log("prepare", prepareStream);
|
||||||
|
if (prepareStream) {
|
||||||
|
this.onWsConnectionMessage({
|
||||||
|
type: "utf8",
|
||||||
|
utf8Data: JSON.stringify({ stream: prepareStream, type: "subscribe" }),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onUserEvent(data: StreamMessages["user"]["payload"]) {
|
private onUserEvent(data: StreamMessages["user"]["payload"]) {
|
||||||
|
@ -125,16 +145,107 @@ export default class Connection {
|
||||||
if (data.type !== "utf8") return;
|
if (data.type !== "utf8") return;
|
||||||
if (data.utf8Data == null) return;
|
if (data.utf8Data == null) return;
|
||||||
|
|
||||||
let obj: Record<string, any>;
|
let objs: Record<string, any>[];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
obj = JSON.parse(data.utf8Data);
|
objs = [JSON.parse(data.utf8Data)];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const simpleObj = objs[0];
|
||||||
|
|
||||||
|
const simpleObj = objs[0];
|
||||||
|
if (simpleObj.stream) {
|
||||||
|
// is Mastodon Compatible
|
||||||
|
this.isMastodonCompatible = true;
|
||||||
|
if (simpleObj.type === "subscribe") {
|
||||||
|
let forSubscribe = [];
|
||||||
|
if (simpleObj.stream === "user") {
|
||||||
|
this.currentSubscribe.push(["user"]);
|
||||||
|
objs = [
|
||||||
|
{
|
||||||
|
type: "connect",
|
||||||
|
body: {
|
||||||
|
channel: "main",
|
||||||
|
id: simpleObj.stream,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "connect",
|
||||||
|
body: {
|
||||||
|
channel: "homeTimeline",
|
||||||
|
id: simpleObj.stream,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const client = getClient(this.host, this.accessToken);
|
||||||
|
try {
|
||||||
|
const tl = await client.getHomeTimeline();
|
||||||
|
for (const t of tl.data) forSubscribe.push(t.id);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(e);
|
||||||
|
console.error(e.response.data);
|
||||||
|
}
|
||||||
|
} else if (simpleObj.stream === "public:local") {
|
||||||
|
this.currentSubscribe.push(["public:local"]);
|
||||||
|
objs = [
|
||||||
|
{
|
||||||
|
type: "connect",
|
||||||
|
body: {
|
||||||
|
channel: "localTimeline",
|
||||||
|
id: simpleObj.stream,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const client = getClient(this.host, this.accessToken);
|
||||||
|
const tl = await client.getLocalTimeline();
|
||||||
|
for (const t of tl.data) forSubscribe.push(t.id);
|
||||||
|
} else if (simpleObj.stream === "public") {
|
||||||
|
this.currentSubscribe.push(["public"]);
|
||||||
|
objs = [
|
||||||
|
{
|
||||||
|
type: "connect",
|
||||||
|
body: {
|
||||||
|
channel: "globalTimeline",
|
||||||
|
id: simpleObj.stream,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const client = getClient(this.host, this.accessToken);
|
||||||
|
const tl = await client.getPublicTimeline();
|
||||||
|
for (const t of tl.data) forSubscribe.push(t.id);
|
||||||
|
} else if (simpleObj.stream === "list") {
|
||||||
|
this.currentSubscribe.push(["list", simpleObj.list]);
|
||||||
|
objs = [
|
||||||
|
{
|
||||||
|
type: "connect",
|
||||||
|
body: {
|
||||||
|
channel: "list",
|
||||||
|
id: simpleObj.stream,
|
||||||
|
params: {
|
||||||
|
listId: simpleObj.list,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const client = getClient(this.host, this.accessToken);
|
||||||
|
const tl = await client.getListTimeline(simpleObj.list);
|
||||||
|
for (const t of tl.data) forSubscribe.push(t.id);
|
||||||
|
}
|
||||||
|
for (const s of forSubscribe) {
|
||||||
|
objs.push({
|
||||||
|
type: "s",
|
||||||
|
body: {
|
||||||
|
id: s,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const obj of objs) {
|
||||||
const { type, body } = obj;
|
const { type, body } = obj;
|
||||||
|
console.log(type, body);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "readNotification":
|
case "readNotification":
|
||||||
this.onReadNotification(body);
|
this.onReadNotification(body);
|
||||||
|
@ -179,6 +290,7 @@ export default class Connection {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onBroadcastMessage(data: StreamMessages["broadcast"]["payload"]) {
|
private onBroadcastMessage(data: StreamMessages["broadcast"]["payload"]) {
|
||||||
this.sendMessageToWs(data.type, data.body);
|
this.sendMessageToWs(data.type, data.body);
|
||||||
|
@ -280,6 +392,68 @@ export default class Connection {
|
||||||
* クライアントにメッセージ送信
|
* クライアントにメッセージ送信
|
||||||
*/
|
*/
|
||||||
public sendMessageToWs(type: string, payload: any) {
|
public sendMessageToWs(type: string, payload: any) {
|
||||||
|
console.log(payload, this.isMastodonCompatible);
|
||||||
|
if (this.isMastodonCompatible) {
|
||||||
|
if (payload.type === "note") {
|
||||||
|
this.wsConnection.send(
|
||||||
|
JSON.stringify({
|
||||||
|
stream: [payload.id],
|
||||||
|
event: "update",
|
||||||
|
payload: JSON.stringify(
|
||||||
|
toTextWithReaction(
|
||||||
|
[Converter.note(payload.body, this.host)],
|
||||||
|
this.host,
|
||||||
|
)[0],
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.onSubscribeNote({
|
||||||
|
id: payload.body.id,
|
||||||
|
});
|
||||||
|
} else if (payload.type === "reacted" || payload.type === "unreacted") {
|
||||||
|
// reaction
|
||||||
|
const client = getClient(this.host, this.accessToken);
|
||||||
|
client.getStatus(payload.id).then((data) => {
|
||||||
|
const newPost = toTextWithReaction([data.data], this.host);
|
||||||
|
for (const stream of this.currentSubscribe) {
|
||||||
|
this.wsConnection.send(
|
||||||
|
JSON.stringify({
|
||||||
|
stream,
|
||||||
|
event: "status.update",
|
||||||
|
payload: JSON.stringify(newPost[0]),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (payload.type === "deleted") {
|
||||||
|
// delete
|
||||||
|
for (const stream of this.currentSubscribe) {
|
||||||
|
this.wsConnection.send(
|
||||||
|
JSON.stringify({
|
||||||
|
stream,
|
||||||
|
event: "delete",
|
||||||
|
payload: payload.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (payload.type === "unreadNotification") {
|
||||||
|
if (payload.id === "user") {
|
||||||
|
const body = Converter.notification(payload.body, this.host);
|
||||||
|
if (body.type === "reaction") body.type = "favourite";
|
||||||
|
body.status = toTextWithReaction(
|
||||||
|
body.status ? [body.status] : [],
|
||||||
|
"",
|
||||||
|
)[0];
|
||||||
|
this.wsConnection.send(
|
||||||
|
JSON.stringify({
|
||||||
|
stream: ["user"],
|
||||||
|
event: "notification",
|
||||||
|
payload: JSON.stringify(body),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
this.wsConnection.send(
|
this.wsConnection.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: type,
|
type: type,
|
||||||
|
@ -287,6 +461,7 @@ export default class Connection {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* チャンネルに接続
|
* チャンネルに接続
|
||||||
|
|
|
@ -16,10 +16,13 @@ export const initializeStreamingServer = (server: http.Server) => {
|
||||||
|
|
||||||
ws.on("request", async (request) => {
|
ws.on("request", async (request) => {
|
||||||
const q = request.resourceURL.query as ParsedUrlQuery;
|
const q = request.resourceURL.query as ParsedUrlQuery;
|
||||||
|
const headers = request.httpRequest.headers['sec-websocket-protocol'] || '';
|
||||||
|
const cred = q.i || q.access_token || headers;
|
||||||
|
const accessToken = cred.toString();
|
||||||
|
|
||||||
const [user, app] = await authenticate(
|
const [user, app] = await authenticate(
|
||||||
request.httpRequest.headers.authorization,
|
request.httpRequest.headers.authorization,
|
||||||
q.i,
|
accessToken,
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
request.reject(403, err.message);
|
request.reject(403, err.message);
|
||||||
return [];
|
return [];
|
||||||
|
@ -43,8 +46,11 @@ export const initializeStreamingServer = (server: http.Server) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
redisClient.on("message", onRedisMessage);
|
redisClient.on("message", onRedisMessage);
|
||||||
|
const host = `https://${request.host}`;
|
||||||
|
const prepareStream = q.stream?.toString();
|
||||||
|
console.log('start', q);
|
||||||
|
|
||||||
const main = new MainStreamConnection(connection, ev, user, app);
|
const main = new MainStreamConnection(connection, ev, user, app, host, accessToken, prepareStream);
|
||||||
|
|
||||||
const intervalId = user
|
const intervalId = user
|
||||||
? setInterval(() => {
|
? setInterval(() => {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { createTemp } from "@/misc/create-temp.js";
|
||||||
import { publishMainStream } from "@/services/stream.js";
|
import { publishMainStream } from "@/services/stream.js";
|
||||||
import * as Acct from "@/misc/acct.js";
|
import * as Acct from "@/misc/acct.js";
|
||||||
import { envOption } from "@/env.js";
|
import { envOption } from "@/env.js";
|
||||||
|
const { koaBody } = require('koa-body');
|
||||||
import megalodon, { MegalodonInterface } from 'megalodon';
|
import megalodon, { MegalodonInterface } from 'megalodon';
|
||||||
import activityPub from "./activitypub.js";
|
import activityPub from "./activitypub.js";
|
||||||
import nodeinfo from "./nodeinfo.js";
|
import nodeinfo from "./nodeinfo.js";
|
||||||
|
@ -140,13 +141,21 @@ router.get("/oauth/authorize", async (ctx) => {
|
||||||
ctx.redirect(Buffer.from(client_id?.toString() || '', 'base64').toString());
|
ctx.redirect(Buffer.from(client_id?.toString() || '', 'base64').toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/oauth/token", async (ctx) => {
|
router.get("/oauth/token", koaBody(), async (ctx) => {
|
||||||
const body: any = ctx.request.body
|
const body: any = ctx.request.body;
|
||||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||||
const generator = (megalodon as any).default;
|
const generator = (megalodon as any).default;
|
||||||
const client = generator('misskey', BASE_URL, null) as MegalodonInterface;
|
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' }
|
||||||
try {
|
try {
|
||||||
ctx.body = await client.fetchAccessToken(null, body.client_secret, body.code);
|
const atData = await client.fetchAccessToken(null, body.client_secret, m[0]);
|
||||||
|
ctx.body = {
|
||||||
|
access_token: atData.accessToken,
|
||||||
|
token_type: 'Bearer',
|
||||||
|
scope: 'read write follow',
|
||||||
|
created_at: new Date().getTime() / 1000
|
||||||
|
};
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
|
|
@ -634,6 +634,10 @@ router.get("/streaming", async (ctx) => {
|
||||||
ctx.status = 503;
|
ctx.status = 503;
|
||||||
ctx.set("Cache-Control", "private, max-age=0");
|
ctx.set("Cache-Control", "private, max-age=0");
|
||||||
});
|
});
|
||||||
|
router.get("/api/v1/streaming", async (ctx) => {
|
||||||
|
ctx.status = 503;
|
||||||
|
ctx.set("Cache-Control", "private, max-age=0");
|
||||||
|
});
|
||||||
|
|
||||||
// Render base html for all requests
|
// Render base html for all requests
|
||||||
router.get("(.*)", async (ctx) => {
|
router.get("(.*)", async (ctx) => {
|
||||||
|
|
|
@ -78,8 +78,9 @@ export default defineComponent({
|
||||||
methods: {
|
methods: {
|
||||||
accepted() {
|
accepted() {
|
||||||
this.state = 'accepted';
|
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) {
|
if (this.session.app.callbackUrl) {
|
||||||
location.href = `${this.session.app.callbackUrl}?token=${this.session.token}&code=${this.session.token}`;
|
location.href = `${this.session.app.callbackUrl}?token=${this.session.token}&code=${this.session.token}&state=${getUrlParams().state || ''}`;
|
||||||
}
|
}
|
||||||
}, onLogin(res) {
|
}, onLogin(res) {
|
||||||
login(res.i);
|
login(res.i);
|
||||||
|
|
145
pnpm-lock.yaml
145
pnpm-lock.yaml
|
@ -60,6 +60,7 @@ importers:
|
||||||
'@bull-board/api': ^4.6.4
|
'@bull-board/api': ^4.6.4
|
||||||
'@bull-board/koa': ^4.6.4
|
'@bull-board/koa': ^4.6.4
|
||||||
'@bull-board/ui': ^4.6.4
|
'@bull-board/ui': ^4.6.4
|
||||||
|
'@cutls/megalodon': 5.1.15
|
||||||
'@discordapp/twemoji': 14.0.2
|
'@discordapp/twemoji': 14.0.2
|
||||||
'@elastic/elasticsearch': 7.17.0
|
'@elastic/elasticsearch': 7.17.0
|
||||||
'@koa/cors': 3.4.3
|
'@koa/cors': 3.4.3
|
||||||
|
@ -120,8 +121,10 @@ importers:
|
||||||
ajv: 8.11.2
|
ajv: 8.11.2
|
||||||
archiver: 5.3.1
|
archiver: 5.3.1
|
||||||
autobind-decorator: 2.4.0
|
autobind-decorator: 2.4.0
|
||||||
|
autolinker: 4.0.0
|
||||||
autwh: 0.1.0
|
autwh: 0.1.0
|
||||||
aws-sdk: 2.1277.0
|
aws-sdk: 2.1277.0
|
||||||
|
axios: ^1.3.2
|
||||||
bcryptjs: 2.4.3
|
bcryptjs: 2.4.3
|
||||||
blurhash: 1.1.5
|
blurhash: 1.1.5
|
||||||
bull: 4.10.2
|
bull: 4.10.2
|
||||||
|
@ -155,6 +158,7 @@ importers:
|
||||||
jsonld: 6.0.0
|
jsonld: 6.0.0
|
||||||
jsrsasign: 10.6.1
|
jsrsasign: 10.6.1
|
||||||
koa: 2.13.4
|
koa: 2.13.4
|
||||||
|
koa-body: ^6.0.1
|
||||||
koa-bodyparser: 4.3.0
|
koa-bodyparser: 4.3.0
|
||||||
koa-favicon: 2.1.0
|
koa-favicon: 2.1.0
|
||||||
koa-json-body: 5.3.0
|
koa-json-body: 5.3.0
|
||||||
|
@ -163,7 +167,6 @@ importers:
|
||||||
koa-send: 5.0.1
|
koa-send: 5.0.1
|
||||||
koa-slow: 2.1.0
|
koa-slow: 2.1.0
|
||||||
koa-views: 7.0.2
|
koa-views: 7.0.2
|
||||||
megalodon: ^5.1.1
|
|
||||||
mfm-js: 0.23.2
|
mfm-js: 0.23.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
mocha: 10.2.0
|
mocha: 10.2.0
|
||||||
|
@ -224,6 +227,7 @@ importers:
|
||||||
'@bull-board/api': 4.10.2
|
'@bull-board/api': 4.10.2
|
||||||
'@bull-board/koa': 4.10.2_6tybghmia4wsnt33xeid7y4rby
|
'@bull-board/koa': 4.10.2_6tybghmia4wsnt33xeid7y4rby
|
||||||
'@bull-board/ui': 4.10.2
|
'@bull-board/ui': 4.10.2
|
||||||
|
'@cutls/megalodon': 5.1.15
|
||||||
'@discordapp/twemoji': 14.0.2
|
'@discordapp/twemoji': 14.0.2
|
||||||
'@elastic/elasticsearch': 7.17.0
|
'@elastic/elasticsearch': 7.17.0
|
||||||
'@koa/cors': 3.4.3
|
'@koa/cors': 3.4.3
|
||||||
|
@ -239,8 +243,10 @@ importers:
|
||||||
ajv: 8.11.2
|
ajv: 8.11.2
|
||||||
archiver: 5.3.1
|
archiver: 5.3.1
|
||||||
autobind-decorator: 2.4.0
|
autobind-decorator: 2.4.0
|
||||||
|
autolinker: 4.0.0
|
||||||
autwh: 0.1.0
|
autwh: 0.1.0
|
||||||
aws-sdk: 2.1277.0
|
aws-sdk: 2.1277.0
|
||||||
|
axios: 1.3.2
|
||||||
bcryptjs: 2.4.3
|
bcryptjs: 2.4.3
|
||||||
blurhash: 1.1.5
|
blurhash: 1.1.5
|
||||||
bull: 4.10.2
|
bull: 4.10.2
|
||||||
|
@ -271,6 +277,7 @@ importers:
|
||||||
jsonld: 6.0.0
|
jsonld: 6.0.0
|
||||||
jsrsasign: 10.6.1
|
jsrsasign: 10.6.1
|
||||||
koa: 2.13.4
|
koa: 2.13.4
|
||||||
|
koa-body: 6.0.1
|
||||||
koa-bodyparser: 4.3.0
|
koa-bodyparser: 4.3.0
|
||||||
koa-favicon: 2.1.0
|
koa-favicon: 2.1.0
|
||||||
koa-json-body: 5.3.0
|
koa-json-body: 5.3.0
|
||||||
|
@ -279,7 +286,6 @@ importers:
|
||||||
koa-send: 5.0.1
|
koa-send: 5.0.1
|
||||||
koa-slow: 2.1.0
|
koa-slow: 2.1.0
|
||||||
koa-views: 7.0.2_6tybghmia4wsnt33xeid7y4rby
|
koa-views: 7.0.2_6tybghmia4wsnt33xeid7y4rby
|
||||||
megalodon: 5.1.1
|
|
||||||
mfm-js: 0.23.2
|
mfm-js: 0.23.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
mocha: 10.2.0
|
mocha: 10.2.0
|
||||||
|
@ -847,6 +853,30 @@ packages:
|
||||||
'@jridgewell/trace-mapping': 0.3.9
|
'@jridgewell/trace-mapping': 0.3.9
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@cutls/megalodon/5.1.15:
|
||||||
|
resolution: {integrity: sha512-4+mIKUYYr2CLY3idSxXk56WSTG9ww3opeenmsPRxftTwcjQTYxGntNkWmJWEbzeJ4rPslnvpwD7cFR62bPf41g==}
|
||||||
|
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
|
||||||
|
|
||||||
/@cypress/request/2.88.11:
|
/@cypress/request/2.88.11:
|
||||||
resolution: {integrity: sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==}
|
resolution: {integrity: sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
@ -2016,6 +2046,13 @@ packages:
|
||||||
cbor: 8.1.0
|
cbor: 8.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/co-body/6.1.0:
|
||||||
|
resolution: {integrity: sha512-3e0q2jyDAnx/DSZi0z2H0yoZ2wt5yRDZ+P7ymcMObvq0ufWRT4tsajyO+Q1VwVWiv9PRR4W3YEjEzBjeZlhF+w==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.11.18
|
||||||
|
'@types/qs': 6.9.7
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/connect/3.4.35:
|
/@types/connect/3.4.35:
|
||||||
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
|
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2083,6 +2120,12 @@ packages:
|
||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.18
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/formidable/2.0.5:
|
||||||
|
resolution: {integrity: sha512-uvMcdn/KK3maPOaVUAc3HEYbCEhjaGFwww4EsX6IJfWIJ1tzHtDHczuImH3GKdusPnAAmzB07St90uabZeCKPA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.11.18
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/glob-stream/6.1.1:
|
/@types/glob-stream/6.1.1:
|
||||||
resolution: {integrity: sha512-AGOUTsTdbPkRS0qDeyeS+6KypmfVpbT5j23SN8UPG63qjKXNKjXn6V9wZUr8Fin0m9l8oGYaPK8b2WUMF8xI1A==}
|
resolution: {integrity: sha512-AGOUTsTdbPkRS0qDeyeS+6KypmfVpbT5j23SN8UPG63qjKXNKjXn6V9wZUr8Fin0m9l8oGYaPK8b2WUMF8xI1A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3202,6 +3245,12 @@ packages:
|
||||||
engines: {node: '>=8.10', npm: '>=6.4.1'}
|
engines: {node: '>=8.10', npm: '>=6.4.1'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/autolinker/4.0.0:
|
||||||
|
resolution: {integrity: sha512-fl5Kh6BmEEZx+IWBfEirnRUU5+cOiV0OK7PEt0RBKvJMJ8GaRseIOeDU3FKf4j3CE5HVefcjHmhYPOcaVt0bZw==}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.4.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/autoprefixer/6.7.7:
|
/autoprefixer/6.7.7:
|
||||||
resolution: {integrity: sha512-WKExI/eSGgGAkWAO+wMVdFObZV7hQen54UpD1kCCTN3tvlL3W1jL4+lPP/M7MwoP7Q4RHzKtO3JQ4HxYEcd+xQ==}
|
resolution: {integrity: sha512-WKExI/eSGgGAkWAO+wMVdFObZV7hQen54UpD1kCCTN3tvlL3W1jL4+lPP/M7MwoP7Q4RHzKtO3JQ4HxYEcd+xQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3253,7 +3302,7 @@ packages:
|
||||||
/axios/0.24.0:
|
/axios/0.24.0:
|
||||||
resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==}
|
resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.2_debug@4.3.4
|
follow-redirects: 1.15.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -3261,7 +3310,7 @@ packages:
|
||||||
/axios/0.25.0_debug@4.3.4:
|
/axios/0.25.0_debug@4.3.4:
|
||||||
resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==}
|
resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.2_debug@4.3.4
|
follow-redirects: 1.15.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -3269,7 +3318,17 @@ packages:
|
||||||
/axios/1.2.2:
|
/axios/1.2.2:
|
||||||
resolution: {integrity: sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==}
|
resolution: {integrity: sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==}
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.2_debug@4.3.4
|
follow-redirects: 1.15.2
|
||||||
|
form-data: 4.0.0
|
||||||
|
proxy-from-env: 1.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/axios/1.3.2:
|
||||||
|
resolution: {integrity: sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==}
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.15.2
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
proxy-from-env: 1.1.0
|
proxy-from-env: 1.1.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
@ -4102,7 +4161,7 @@ packages:
|
||||||
resolution: {integrity: sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ==}
|
resolution: {integrity: sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
inflation: 2.0.0
|
inflation: 2.0.0
|
||||||
qs: 6.10.4
|
qs: 6.11.0
|
||||||
raw-body: 2.5.1
|
raw-body: 2.5.1
|
||||||
type-is: 1.6.18
|
type-is: 1.6.18
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -4111,7 +4170,7 @@ packages:
|
||||||
resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==}
|
resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
inflation: 2.0.0
|
inflation: 2.0.0
|
||||||
qs: 6.10.4
|
qs: 6.11.0
|
||||||
raw-body: 2.5.1
|
raw-body: 2.5.1
|
||||||
type-is: 1.6.18
|
type-is: 1.6.18
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -5232,6 +5291,13 @@ packages:
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/dezalgo/1.0.4:
|
||||||
|
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
|
||||||
|
dependencies:
|
||||||
|
asap: 2.0.6
|
||||||
|
wrappy: 1.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/diff/4.0.2:
|
/diff/4.0.2:
|
||||||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
||||||
engines: {node: '>=0.3.1'}
|
engines: {node: '>=0.3.1'}
|
||||||
|
@ -6213,7 +6279,7 @@ packages:
|
||||||
readable-stream: 2.3.7
|
readable-stream: 2.3.7
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/follow-redirects/1.15.2_debug@4.3.4:
|
/follow-redirects/1.15.2:
|
||||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -6221,8 +6287,6 @@ packages:
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
debug:
|
debug:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
|
||||||
debug: 4.3.4
|
|
||||||
|
|
||||||
/for-each/0.3.3:
|
/for-each/0.3.3:
|
||||||
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
||||||
|
@ -6282,6 +6346,15 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
fetch-blob: 3.2.0
|
fetch-blob: 3.2.0
|
||||||
|
|
||||||
|
/formidable/2.1.1:
|
||||||
|
resolution: {integrity: sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==}
|
||||||
|
dependencies:
|
||||||
|
dezalgo: 1.0.4
|
||||||
|
hexoid: 1.0.0
|
||||||
|
once: 1.4.0
|
||||||
|
qs: 6.11.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/fragment-cache/0.2.1:
|
/fragment-cache/0.2.1:
|
||||||
resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
|
resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -6932,6 +7005,11 @@ packages:
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/hexoid/1.0.0:
|
||||||
|
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/highlight.js/10.7.3:
|
/highlight.js/10.7.3:
|
||||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -7999,6 +8077,17 @@ packages:
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/koa-body/6.0.1:
|
||||||
|
resolution: {integrity: sha512-M8ZvMD8r+kPHy28aWP9VxL7kY8oPWA+C7ZgCljrCMeaU7uX6wsIQgDHskyrAr9sw+jqnIXyv4Mlxri5R4InIJg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/co-body': 6.1.0
|
||||||
|
'@types/formidable': 2.0.5
|
||||||
|
'@types/koa': 2.13.5
|
||||||
|
co-body: 6.1.0
|
||||||
|
formidable: 2.1.1
|
||||||
|
zod: 3.20.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/koa-bodyparser/4.3.0:
|
/koa-bodyparser/4.3.0:
|
||||||
resolution: {integrity: sha512-uyV8G29KAGwZc4q/0WUAjH+Tsmuv9ImfBUF2oZVyZtaeo0husInagyn/JH85xMSxM0hEk/mbCII5ubLDuqW/Rw==}
|
resolution: {integrity: sha512-uyV8G29KAGwZc4q/0WUAjH+Tsmuv9ImfBUF2oZVyZtaeo0husInagyn/JH85xMSxM0hEk/mbCII5ubLDuqW/Rw==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
@ -8672,30 +8761,6 @@ packages:
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: false
|
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:
|
/merge-stream/2.0.0:
|
||||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||||
|
|
||||||
|
@ -10473,6 +10538,14 @@ packages:
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel: 1.0.4
|
side-channel: 1.0.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/qs/6.11.0:
|
||||||
|
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
dependencies:
|
||||||
|
side-channel: 1.0.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/qs/6.5.3:
|
/qs/6.5.3:
|
||||||
resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
|
resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
|
||||||
|
@ -13230,6 +13303,10 @@ packages:
|
||||||
resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==}
|
resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/zod/3.20.3:
|
||||||
|
resolution: {integrity: sha512-+MLeeUcLTlnzVo5xDn9+LVN9oX4esvgZ7qfZczBN+YVUvZBafIrPPVyG2WdjMWU2Qkb2ZAh2M8lpqf1wIoGqJQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
github.com/misskey-dev/browser-image-resizer/0380d12c8e736788ea7f4e6e985175521ea7b23c:
|
github.com/misskey-dev/browser-image-resizer/0380d12c8e736788ea7f4e6e985175521ea7b23c:
|
||||||
resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/0380d12c8e736788ea7f4e6e985175521ea7b23c}
|
resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/0380d12c8e736788ea7f4e6e985175521ea7b23c}
|
||||||
name: browser-image-resizer
|
name: browser-image-resizer
|
||||||
|
|
Loading…
Reference in a new issue