summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Dockerfile1
-rw-r--r--packages/backend/package.json2
-rw-r--r--packages/backend/src/server/ServerModule.ts2
-rw-r--r--packages/backend/src/server/ServerService.ts3
-rw-r--r--packages/backend/src/server/api/mastodon/MastodonApiServerService.ts192
-rw-r--r--packages/backend/src/server/api/mastodon/converters.ts136
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/meta.ts63
-rw-r--r--packages/megalodon/package.json83
-rw-r--r--packages/megalodon/src/axios.d.ts1
-rw-r--r--packages/megalodon/src/cancel.ts13
-rw-r--r--packages/megalodon/src/converter.ts3
-rw-r--r--packages/megalodon/src/default.ts3
-rw-r--r--packages/megalodon/src/entities/account.ts27
-rw-r--r--packages/megalodon/src/entities/activity.ts8
-rw-r--r--packages/megalodon/src/entities/announcement.ts34
-rw-r--r--packages/megalodon/src/entities/application.ts7
-rw-r--r--packages/megalodon/src/entities/async_attachment.ts14
-rw-r--r--packages/megalodon/src/entities/attachment.ts49
-rw-r--r--packages/megalodon/src/entities/card.ts16
-rw-r--r--packages/megalodon/src/entities/context.ts8
-rw-r--r--packages/megalodon/src/entities/conversation.ts11
-rw-r--r--packages/megalodon/src/entities/emoji.ts9
-rw-r--r--packages/megalodon/src/entities/featured_tag.ts8
-rw-r--r--packages/megalodon/src/entities/field.ts7
-rw-r--r--packages/megalodon/src/entities/filter.ts12
-rw-r--r--packages/megalodon/src/entities/history.ts7
-rw-r--r--packages/megalodon/src/entities/identity_proof.ts9
-rw-r--r--packages/megalodon/src/entities/instance.ts41
-rw-r--r--packages/megalodon/src/entities/list.ts6
-rw-r--r--packages/megalodon/src/entities/marker.ts15
-rw-r--r--packages/megalodon/src/entities/mention.ts8
-rw-r--r--packages/megalodon/src/entities/notification.ts15
-rw-r--r--packages/megalodon/src/entities/poll.ts14
-rw-r--r--packages/megalodon/src/entities/poll_option.ts6
-rw-r--r--packages/megalodon/src/entities/preferences.ts9
-rw-r--r--packages/megalodon/src/entities/push_subscription.ts16
-rw-r--r--packages/megalodon/src/entities/reaction.ts12
-rw-r--r--packages/megalodon/src/entities/relationship.ts17
-rw-r--r--packages/megalodon/src/entities/report.ts9
-rw-r--r--packages/megalodon/src/entities/results.ts11
-rw-r--r--packages/megalodon/src/entities/scheduled_status.ts10
-rw-r--r--packages/megalodon/src/entities/source.ts10
-rw-r--r--packages/megalodon/src/entities/stats.ts7
-rw-r--r--packages/megalodon/src/entities/status.ts45
-rw-r--r--packages/megalodon/src/entities/status_edit.ts23
-rw-r--r--packages/megalodon/src/entities/status_params.ts12
-rw-r--r--packages/megalodon/src/entities/tag.ts10
-rw-r--r--packages/megalodon/src/entities/token.ts8
-rw-r--r--packages/megalodon/src/entities/urls.ts5
-rw-r--r--packages/megalodon/src/entity.ts38
-rw-r--r--packages/megalodon/src/filter_context.ts11
-rw-r--r--packages/megalodon/src/index.ts32
-rw-r--r--packages/megalodon/src/megalodon.ts1532
-rw-r--r--packages/megalodon/src/misskey.ts3436
-rw-r--r--packages/megalodon/src/misskey/api_client.ts727
-rw-r--r--packages/megalodon/src/misskey/entities/GetAll.ts6
-rw-r--r--packages/megalodon/src/misskey/entities/announcement.ts10
-rw-r--r--packages/megalodon/src/misskey/entities/app.ts9
-rw-r--r--packages/megalodon/src/misskey/entities/blocking.ts10
-rw-r--r--packages/megalodon/src/misskey/entities/createdNote.ts7
-rw-r--r--packages/megalodon/src/misskey/entities/emoji.ts9
-rw-r--r--packages/megalodon/src/misskey/entities/favorite.ts10
-rw-r--r--packages/megalodon/src/misskey/entities/field.ts7
-rw-r--r--packages/megalodon/src/misskey/entities/file.ts20
-rw-r--r--packages/megalodon/src/misskey/entities/followRequest.ts9
-rw-r--r--packages/megalodon/src/misskey/entities/follower.ts11
-rw-r--r--packages/megalodon/src/misskey/entities/following.ts11
-rw-r--r--packages/megalodon/src/misskey/entities/hashtag.ts7
-rw-r--r--packages/megalodon/src/misskey/entities/list.ts8
-rw-r--r--packages/megalodon/src/misskey/entities/meta.ts18
-rw-r--r--packages/megalodon/src/misskey/entities/mute.ts10
-rw-r--r--packages/megalodon/src/misskey/entities/note.ts32
-rw-r--r--packages/megalodon/src/misskey/entities/notification.ts17
-rw-r--r--packages/megalodon/src/misskey/entities/poll.ts13
-rw-r--r--packages/megalodon/src/misskey/entities/reaction.ts11
-rw-r--r--packages/megalodon/src/misskey/entities/relation.ts12
-rw-r--r--packages/megalodon/src/misskey/entities/session.ts6
-rw-r--r--packages/megalodon/src/misskey/entities/state.ts7
-rw-r--r--packages/megalodon/src/misskey/entities/stats.ts9
-rw-r--r--packages/megalodon/src/misskey/entities/user.ts13
-rw-r--r--packages/megalodon/src/misskey/entities/userDetail.ts34
-rw-r--r--packages/megalodon/src/misskey/entities/userDetailMe.ts36
-rw-r--r--packages/megalodon/src/misskey/entities/userkey.ts8
-rw-r--r--packages/megalodon/src/misskey/entity.ts28
-rw-r--r--packages/megalodon/src/misskey/notification.ts18
-rw-r--r--packages/megalodon/src/misskey/web_socket.ts458
-rw-r--r--packages/megalodon/src/notification.ts14
-rw-r--r--packages/megalodon/src/oauth.ts123
-rw-r--r--packages/megalodon/src/parser.ts94
-rw-r--r--packages/megalodon/src/proxy_config.ts92
-rw-r--r--packages/megalodon/src/response.ts8
-rw-r--r--packages/megalodon/test/integration/megalodon.spec.ts27
-rw-r--r--packages/megalodon/test/integration/misskey.spec.ts204
-rw-r--r--packages/megalodon/test/unit/misskey/api_client.spec.ts233
-rw-r--r--packages/megalodon/test/unit/parser.spec.ts152
-rw-r--r--packages/megalodon/tsconfig.json64
-rw-r--r--packages/sw/src/scripts/create-notification.ts2
-rw-r--r--pnpm-lock.yaml871
-rw-r--r--pnpm-workspace.yaml1
-rw-r--r--scripts/clean-all.js2
-rw-r--r--scripts/clean.js1
-rw-r--r--scripts/dev.mjs6
103 files changed, 9492 insertions, 82 deletions
diff --git a/.gitignore b/.gitignore
index a66e527db0..11e69b2621 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,6 +58,9 @@ ormconfig.json
temp
/packages/frontend/src/**/*.stories.ts
+# Sharkey
+/packages/megalodon/lib
+
# blender backups
*.blend1
*.blend2
diff --git a/Dockerfile b/Dockerfile
index a417355cfa..76e99a9dd0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -24,6 +24,7 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"]
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
+COPY --link ["packages/megalodon/package.json", "./packages/megalodon/"]
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm i --frozen-lockfile --aggregate-output
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 3d3fc87009..1c2ffcfb6d 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -99,6 +99,7 @@
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"fastify": "4.23.2",
+ "fastify-multer": "^2.0.3",
"feed": "4.2.2",
"file-type": "18.5.0",
"fluent-ffmpeg": "2.1.2",
@@ -116,6 +117,7 @@
"json5": "2.2.3",
"jsonld": "8.3.1",
"jsrsasign": "10.8.6",
+ "megalodon": "workspace:*",
"meilisearch": "0.34.2",
"mfm-js": "0.23.3",
"microformats-parser": "1.5.2",
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index fa81380f01..fc6f019602 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -39,6 +39,7 @@ import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
import { UserListChannelService } from './api/stream/channels/user-list.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
+import { MastodonApiServerService } from './api/mastodon/MastodonApiServerService.js';
import { ClientLoggerService } from './web/ClientLoggerService.js';
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
@@ -84,6 +85,7 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
ServerStatsChannelService,
UserListChannelService,
OpenApiServerService,
+ MastodonApiServerService,
OAuth2ProviderService,
],
exports: [
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 0e4a5ece3e..a1189e2198 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -30,6 +30,7 @@ import { WellKnownServerService } from './WellKnownServerService.js';
import { FileServerService } from './FileServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
+import { MastodonApiServerService } from './api/mastodon/MastodonApiServerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
const _dirname = fileURLToPath(new URL('.', import.meta.url));
@@ -56,6 +57,7 @@ export class ServerService implements OnApplicationShutdown {
private userEntityService: UserEntityService,
private apiServerService: ApiServerService,
private openApiServerService: OpenApiServerService,
+ private mastodonApiServerService: MastodonApiServerService,
private streamingApiServerService: StreamingApiServerService,
private activityPubServerService: ActivityPubServerService,
private wellKnownServerService: WellKnownServerService,
@@ -95,6 +97,7 @@ export class ServerService implements OnApplicationShutdown {
fastify.register(this.apiServerService.createServer, { prefix: '/api' });
fastify.register(this.openApiServerService.createServer);
+ fastify.register(this.mastodonApiServerService.createServer, { prefix: '/api' });
fastify.register(this.fileServerService.createServer);
fastify.register(this.activityPubServerService.createServer);
fastify.register(this.nodeinfoServerService.createServer);
diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
new file mode 100644
index 0000000000..b79489d18d
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
@@ -0,0 +1,192 @@
+import { fileURLToPath } from 'node:url';
+import { Inject, Injectable } from '@nestjs/common';
+import type { UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import megalodon, { MegalodonInterface } from "megalodon";
+import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
+import { convertId, IdConvertType as IdType, convertAccount, convertAnnouncement, convertFilter, convertAttachment } from './converters.js';
+import { IsNull } from 'typeorm';
+import type { Config } from '@/config.js';
+import { getInstance } from './endpoints/meta.js';
+import { MetaService } from '@/core/MetaService.js';
+import multer from 'fastify-multer';
+
+const staticAssets = fileURLToPath(new URL('../../../../assets/', import.meta.url));
+
+export function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface {
+ const accessTokenArr = authorization?.split(" ") ?? [null];
+ const accessToken = accessTokenArr[accessTokenArr.length - 1];
+ const generator = (megalodon as any).default;
+ const client = generator(BASE_URL, accessToken) as MegalodonInterface;
+ return client;
+}
+
+@Injectable()
+export class MastodonApiServerService {
+ constructor(
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+ @Inject(DI.config)
+ private config: Config,
+ private metaService: MetaService,
+ ) { }
+
+ @bindThis
+ public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
+ const upload = multer({
+ storage: multer.diskStorage({}),
+ limits: {
+ fileSize: this.config.maxFileSize || 262144000,
+ files: 1,
+ },
+ });
+
+ fastify.register(multer.contentParser);
+
+ fastify.get("/v1/custom_emojis", async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const accessTokens = _request.headers.authorization;
+ const client = getClient(BASE_URL, accessTokens);
+ try {
+ const data = await client.getInstanceCustomEmojis();
+ reply.send(data.data);
+ } catch (e: any) {
+ console.error(e);
+ reply.code(401).send(e.response.data);
+ }
+ });
+
+ fastify.get("/v1/instance", async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const accessTokens = _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();
+ const admin = await this.usersRepository.findOne({
+ where: {
+ host: IsNull(),
+ isRoot: true,
+ isDeleted: false,
+ isSuspended: false,
+ },
+ order: { id: "ASC" },
+ });
+ const contact = admin == null ? null : convertAccount((await client.getAccount(admin.id)).data);
+ reply.send(await getInstance(data.data, contact, this.config, await this.metaService.fetch()));
+ } catch (e: any) {
+ console.error(e);
+ reply.code(401).send(e.response.data);
+ }
+ });
+
+ fastify.get("/v1/announcements", async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const accessTokens = _request.headers.authorization;
+ const client = getClient(BASE_URL, accessTokens);
+ try {
+ const data = await client.getInstanceAnnouncements();
+ reply.send(data.data.map((announcement) => convertAnnouncement(announcement)));
+ } catch (e: any) {
+ console.error(e);
+ reply.code(401).send(e.response.data);
+ }
+ });
+
+ fastify.post<{ Body: { id: string } }>("/v1/announcements/:id/dismiss", async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const accessTokens = _request.headers.authorization;
+ const client = getClient(BASE_URL, accessTokens);
+ try {
+ const data = await client.dismissInstanceAnnouncement(
+ convertId(_request.body['id'], IdType.SharkeyId)
+ );
+ reply.send(data.data);
+ } catch (e: any) {
+ console.error(e);
+ reply.code(401).send(e.response.data);
+ }
+ },
+ );
+
+ fastify.post("/v1/media", { preHandler: upload.single('file') }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const accessTokens = _request.headers.authorization;
+ const client = getClient(BASE_URL, accessTokens);
+ try {
+ const multipartData = await _request.file;
+ if (!multipartData) {
+ reply.code(401).send({ error: "No image" });
+ return;
+ }
+ const data = await client.uploadMedia(multipartData);
+ reply.send(convertAttachment(data.data as Entity.Attachment));
+ } catch (e: any) {
+ console.error(e);
+ reply.code(401).send(e.response.data);
+ }
+ });
+
+ fastify.post("/v2/media", { preHandler: upload.single('file') }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const accessTokens = _request.headers.authorization;
+ const client = getClient(BASE_URL, accessTokens);
+ try {
+ const multipartData = await _request.file;
+ if (!multipartData) {
+ reply.code(401).send({ error: "No image" });
+ return;
+ }
+ const data = await client.uploadMedia(multipartData, _request.body!);
+ reply.send(convertAttachment(data.data as Entity.Attachment));
+ } catch (e: any) {
+ console.error(e);
+ reply.code(401).send(e.response.data);
+ }
+ });
+
+ fastify.get("/v1/filters", async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const accessTokens = _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.getFilters();
+ reply.send(data.data.map((filter) => convertFilter(filter)));
+ } catch (e: any) {
+ console.error(e);
+ reply.code(401).send(e.response.data);
+ }
+ });
+
+ fastify.get("/v1/trends", async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const accessTokens = _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.getInstanceTrends();
+ reply.send(data.data);
+ } catch (e: any) {
+ console.error(e);
+ reply.code(401).send(e.response.data);
+ }
+ });
+
+ fastify.get("/v1/preferences", async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const accessTokens = _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.getPreferences();
+ reply.send(data.data);
+ } catch (e: any) {
+ console.error(e);
+ reply.code(401).send(e.response.data);
+ }
+ });
+ done();
+ }
+} \ No newline at end of file
diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts
new file mode 100644
index 0000000000..94b70230d8
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/converters.ts
@@ -0,0 +1,136 @@
+import { Entity } from "megalodon";
+
+const CHAR_COLLECTION: string = "0123456789abcdefghijklmnopqrstuvwxyz";
+
+export enum IdConvertType {
+ MastodonId,
+ SharkeyId,
+}
+
+export function convertId(in_id: string, id_convert_type: IdConvertType): string {
+ switch (id_convert_type) {
+ case IdConvertType.MastodonId:
+ let out: bigint = BigInt(0);
+ const lowerCaseId = in_id.toLowerCase();
+ for (let i = 0; i < lowerCaseId.length; i++) {
+ const charValue = numFromChar(lowerCaseId.charAt(i));
+ out += BigInt(charValue) * BigInt(36) ** BigInt(i);
+ }
+ return out.toString();
+
+ case IdConvertType.SharkeyId:
+ let input: bigint = BigInt(in_id);
+ let outStr = '';
+ while (input > BigInt(0)) {
+ const remainder = Number(input % BigInt(36));
+ outStr = charFromNum(remainder) + outStr;
+ input /= BigInt(36);
+ }
+ return outStr;
+
+ default:
+ throw new Error('Invalid ID conversion type');
+ }
+}
+
+function numFromChar(character: string): number {
+ for (let i = 0; i < CHAR_COLLECTION.length; i++) {
+ if (CHAR_COLLECTION.charAt(i) === character) {
+ return i;
+ }
+ }
+
+ throw new Error('Invalid character in parsed base36 id');
+}
+
+function charFromNum(number: number): string {
+ if (number >= 0 && number < CHAR_COLLECTION.length) {
+ return CHAR_COLLECTION.charAt(number);
+ } else {
+ throw new Error('Invalid number for base-36 encoding');
+ }
+}
+
+function simpleConvert(data: any) {
+ // copy the object to bypass weird pass by reference bugs
+ const result = Object.assign({}, data);
+ result.id = convertId(data.id, IdConvertType.MastodonId);
+ return result;
+}
+
+export function convertAccount(account: Entity.Account) {
+ return simpleConvert(account);
+}
+export function convertAnnouncement(announcement: Entity.Announcement) {
+ return simpleConvert(announcement);
+}
+export function convertAttachment(attachment: Entity.Attachment) {
+ return simpleConvert(attachment);
+}
+export function convertFilter(filter: Entity.Filter) {
+ return simpleConvert(filter);
+}
+export function convertList(list: Entity.List) {
+ return simpleConvert(list);
+}
+export function convertFeaturedTag(tag: Entity.FeaturedTag) {
+ return simpleConvert(tag);
+}
+
+export function convertNotification(notification: Entity.Notification) {
+ notification.account = convertAccount(notification.account);
+ notification.id = convertId(notification.id, IdConvertType.MastodonId);
+ if (notification.status)
+ notification.status = convertStatus(notification.status);
+ if (notification.reaction)
+ notification.reaction = convertReaction(notification.reaction);
+ return notification;
+}
+
+export function convertPoll(poll: Entity.Poll) {
+ return simpleConvert(poll);
+}
+export function convertReaction(reaction: Entity.Reaction) {
+ if (reaction.accounts) {
+ reaction.accounts = reaction.accounts.map(convertAccount);
+ }
+ return reaction;
+}
+export function convertRelationship(relationship: Entity.Relationship) {
+ return simpleConvert(relationship);
+}
+
+export function convertStatus(status: Entity.Status) {
+ status.account = convertAccount(status.account);
+ status.id = convertId(status.id, IdConvertType.MastodonId);
+ if (status.in_reply_to_account_id)
+ status.in_reply_to_account_id = convertId(
+ status.in_reply_to_account_id,
+ IdConvertType.MastodonId,
+ );
+ if (status.in_reply_to_id)
+ status.in_reply_to_id = convertId(status.in_reply_to_id, IdConvertType.MastodonId);
+ status.media_attachments = status.media_attachments.map((attachment) =>
+ convertAttachment(attachment),
+ );
+ status.mentions = status.mentions.map((mention) => ({
+ ...mention,
+ id: convertId(mention.id, IdConvertType.MastodonId),
+ }));
+ if (status.poll) status.poll = convertPoll(status.poll);
+ if (status.reblog) status.reblog = convertStatus(status.reblog);
+ if (status.quote) status.quote = convertStatus(status.quote);
+ status.reactions = status.reactions.map(convertReaction);
+
+ return status;
+}
+
+export function convertConversation(conversation: Entity.Conversation) {
+ conversation.id = convertId(conversation.id, IdConvertType.MastodonId);
+ conversation.accounts = conversation.accounts.map(convertAccount);
+ if (conversation.last_status) {
+ conversation.last_status = convertStatus(conversation.last_status);
+ }
+
+ return conversation;
+}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
new file mode 100644
index 0000000000..a37742a068
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
@@ -0,0 +1,63 @@
+import { Entity } from "megalodon";
+import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from "@/const.js";
+import type { Config } from '@/config.js';
+import type { MiMeta } from "@/models/Meta.js";
+
+export async function getInstance(
+ response: Entity.Instance,
+ contact: Entity.Account,
+ config: Config,
+ meta: MiMeta,
+) {
+ return {
+ uri: config.url,
+ title: meta.name || "Sharkey",
+ short_description:
+ meta.description?.substring(0, 50) || "See real server website",
+ description:
+ meta.description ||
+ "This is a vanilla Sharkey Instance. It doesn't seem to have a description.",
+ email: response.email || "",
+ version: `3.0.0 (compatible; Sharkey ${config.version})`,
+ urls: response.urls,
+ stats: {
+ user_count: response.stats.user_count,
+ status_count: response.stats.status_count,
+ domain_count: response.stats.domain_count,
+ },
+ thumbnail: meta.backgroundImageUrl || "/static-assets/transparent.png",
+ languages: meta.langs,
+ registrations: !meta.disableRegistration || response.registrations,
+ approval_required: !response.registrations,
+ invites_enabled: response.registrations,
+ configuration: {
+ accounts: {
+ max_featured_tags: 20,
+ },
+ statuses: {
+ max_characters: MAX_NOTE_TEXT_LENGTH,
+ max_media_attachments: 16,
+ characters_reserved_per_url: response.uri.length,
+ },
+ media_attachments: {
+ supported_mime_types: FILE_TYPE_BROWSERSAFE,
+ image_size_limit: 10485760,
+ image_matrix_limit: 16777216,
+ video_size_limit: 41943040,
+ video_frame_rate_limit: 60,
+ video_matrix_limit: 2304000,
+ },
+ polls: {
+ max_options: 10,
+ max_characters_per_option: 50,
+ min_expiration: 50,
+ max_expiration: 2629746,
+ },
+ reactions: {
+ max_reactions: 1,
+ },
+ },
+ contact_account: contact,
+ rules: [],
+ };
+} \ No newline at end of file
diff --git a/packages/megalodon/package.json b/packages/megalodon/package.json
new file mode 100644
index 0000000000..3403b94b47
--- /dev/null
+++ b/packages/megalodon/package.json
@@ -0,0 +1,83 @@
+{
+ "name": "megalodon",
+ "private": true,
+ "main": "./lib/src/index.js",
+ "typings": "./lib/src/index.d.ts",
+ "scripts": {
+ "build": "tsc -p ./",
+ "build:debug": "pnpm run build",
+ "lint": "pnpm biome check **/*.ts --apply",
+ "format": "pnpm biome format --write src/**/*.ts",
+ "doc": "typedoc --out ../docs ./src",
+ "test": "NODE_ENV=test jest -u --maxWorkers=3"
+ },
+ "jest": {
+ "moduleFileExtensions": [
+ "ts",
+ "js"
+ ],
+ "moduleNameMapper": {
+ "^@/(.+)": "<rootDir>/src/$1",
+ "^~/(.+)": "<rootDir>/$1"
+ },
+ "testMatch": [
+ "**/test/**/*.spec.ts"
+ ],
+ "preset": "ts-jest/presets/default",
+ "transform": {
+ "^.+\\.(ts|tsx)$": "ts-jest"
+ },
+ "globals": {
+ "ts-jest": {
+ "tsconfig": "tsconfig.json"
+ }
+ },
+ "testEnvironment": "node"
+ },
+ "dependencies": {
+ "@types/oauth": "^0.9.0",
+ "@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",
+ "async-lock": "1.4.0"
+ },
+ "devDependencies": {
+ "@types/core-js": "^2.5.0",
+ "@types/form-data": "^2.5.0",
+ "@types/jest": "^29.4.0",
+ "@types/object-assign-deep": "^0.4.0",
+ "@types/parse-link-header": "^2.0.0",
+ "@types/uuid": "^9.0.0",
+ "@types/node": "18.11.18",
+ "@typescript-eslint/eslint-plugin": "^5.49.0",
+ "@typescript-eslint/parser": "^5.49.0",
+ "@types/async-lock": "1.4.0",
+ "eslint": "^8.32.0",
+ "eslint-config-prettier": "^8.6.0",
+ "eslint-config-standard": "^16.0.3",
+ "eslint-plugin-import": "^2.27.5",
+ "eslint-plugin-node": "^11.0.0",
+ "eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-promise": "^6.1.1",
+ "eslint-plugin-standard": "^5.0.0",
+ "jest": "^29.4.0",
+ "jest-worker": "^29.4.0",
+ "lodash": "^4.17.14",
+ "prettier": "^2.8.3",
+ "ts-jest": "^29.0.5",
+ "typedoc": "^0.23.24"
+ },
+ "directories": {
+ "lib": "lib",
+ "test": "test"
+ }
+}
diff --git a/packages/megalodon/src/axios.d.ts b/packages/megalodon/src/axios.d.ts
new file mode 100644
index 0000000000..f19fe38a2b
--- /dev/null
+++ b/packages/megalodon/src/axios.d.ts
@@ -0,0 +1 @@
+declare module "axios/lib/adapters/http";
diff --git a/packages/megalodon/src/cancel.ts b/packages/megalodon/src/cancel.ts
new file mode 100644
index 0000000000..f8e4729b8e
--- /dev/null
+++ b/packages/megalodon/src/cancel.ts
@@ -0,0 +1,13 @@
+export class RequestCanceledError extends Error {
+ public isCancel: boolean;
+
+ constructor(msg: string) {
+ super(msg);
+ this.isCancel = true;
+ Object.setPrototypeOf(this, RequestCanceledError);
+ }
+}
+
+export const isCancel = (value: any): boolean => {
+ return value && value.isCancel;
+};
diff --git a/packages/megalodon/src/converter.ts b/packages/megalodon/src/converter.ts
new file mode 100644
index 0000000000..93d669fa7d
--- /dev/null
+++ b/packages/megalodon/src/converter.ts
@@ -0,0 +1,3 @@
+import MisskeyAPI from "./misskey/api_client";
+
+export default MisskeyAPI.Converter;
diff --git a/packages/megalodon/src/default.ts b/packages/megalodon/src/default.ts
new file mode 100644
index 0000000000..45bce13e21
--- /dev/null
+++ b/packages/megalodon/src/default.ts
@@ -0,0 +1,3 @@
+export const NO_REDIRECT = "urn:ietf:wg:oauth:2.0:oob";
+export const DEFAULT_SCOPE = ["read", "write", "follow"];
+export const DEFAULT_UA = "megalodon";
diff --git a/packages/megalodon/src/entities/account.ts b/packages/megalodon/src/entities/account.ts
new file mode 100644
index 0000000000..06a85eb98e
--- /dev/null
+++ b/packages/megalodon/src/entities/account.ts
@@ -0,0 +1,27 @@
+/// <reference path="emoji.ts" />
+/// <reference path="source.ts" />
+/// <reference path="field.ts" />
+namespace Entity {
+ export type Account = {
+ id: string;
+ username: string;
+ acct: string;
+ display_name: string;
+ locked: boolean;
+ created_at: string;
+ followers_count: number;
+ following_count: number;
+ statuses_count: number;
+ note: string;
+ url: string;
+ avatar: string;
+ avatar_static: string;
+ header: string;
+ header_static: string;
+ emojis: Array<Emoji>;
+ moved: Account | null;
+ fields: Array<Field>;
+ bot: boolean | null;
+ source?: Source;
+ };
+}
diff --git a/packages/megalodon/src/entities/activity.ts b/packages/megalodon/src/entities/activity.ts
new file mode 100644
index 0000000000..6bc0b6d80e
--- /dev/null
+++ b/packages/megalodon/src/entities/activity.ts
@@ -0,0 +1,8 @@
+namespace Entity {
+ export type Activity = {
+ week: string;
+ statuses: string;
+ logins: string;
+ registrations: string;
+ };
+}
diff --git a/packages/megalodon/src/entities/announcement.ts b/packages/megalodon/src/entities/announcement.ts
new file mode 100644
index 0000000000..7c79831634
--- /dev/null
+++ b/packages/megalodon/src/entities/announcement.ts
@@ -0,0 +1,34 @@
+/// <reference path="tag.ts" />
+/// <reference path="emoji.ts" />
+/// <reference path="reaction.ts" />
+
+namespace Entity {
+ export type Announcement = {
+ id: string;
+ content: string;
+ starts_at: string | null;
+ ends_at: string | null;
+ published: boolean;
+ all_day: boolean;
+ published_at: string;
+ updated_at: string;
+ read?: boolean;
+ mentions: Array<AnnouncementAccount>;
+ statuses: Array<AnnouncementStatus>;
+ tags: Array<Tag>;
+ emojis: Array<Emoji>;
+ reactions: Array<Reaction>;
+ };
+
+ export type AnnouncementAccount = {
+ id: string;
+ username: string;
+ url: string;
+ acct: string;
+ };
+
+ export type AnnouncementStatus = {
+ id: string;
+ url: string;
+ };
+}
diff --git a/packages/megalodon/src/entities/application.ts b/packages/megalodon/src/entities/application.ts
new file mode 100644
index 0000000000..9b98b12772
--- /dev/null
+++ b/packages/megalodon/src/entities/application.ts
@@ -0,0 +1,7 @@
+namespace Entity {
+ export type Application = {
+ name: string;
+ website?: string | null;
+ vapid_key?: string | null;
+ };
+}
diff --git a/packages/megalodon/src/entities/async_attachment.ts b/packages/megalodon/src/entities/async_attachment.ts
new file mode 100644
index 0000000000..9cc17acc5c
--- /dev/null
+++ b/packages/megalodon/src/entities/async_attachment.ts
@@ -0,0 +1,14 @@
+/// <reference path="attachment.ts" />
+namespace Entity {
+ export type AsyncAttachment = {
+ id: string;
+ type: "unknown" | "image" | "gifv" | "video" | "audio";
+ url: string | null;
+ remote_url: string | null;
+ preview_url: string;
+ text_url: string | null;
+ meta: Meta | null;
+ description: string | null;
+ blurhash: string | null;
+ };
+}
diff --git a/packages/megalodon/src/entities/attachment.ts b/packages/megalodon/src/entities/attachment.ts
new file mode 100644
index 0000000000..082c79eddb
--- /dev/null
+++ b/packages/megalodon/src/entities/attachment.ts
@@ -0,0 +1,49 @@
+namespace Entity {
+ export type Sub = {
+ // For Image, Gifv, and Video
+ width?: number;
+ height?: number;
+ size?: string;
+ aspect?: number;
+
+ // For Gifv and Video
+ frame_rate?: string;
+
+ // For Audio, Gifv, and Video
+ duration?: number;
+ bitrate?: number;
+ };
+
+ export type Focus = {
+ x: number;
+ y: number;
+ };
+
+ export type Meta = {
+ original?: Sub;
+ small?: Sub;
+ focus?: Focus;
+ length?: string;
+ duration?: number;
+ fps?: number;
+ size?: string;
+ width?: number;
+ height?: number;
+ aspect?: number;
+ audio_encode?: string;
+ audio_bitrate?: string;
+ audio_channel?: string;
+ };
+
+ export type Attachment = {
+ id: string;
+ type: "unknown" | "image" | "gifv" | "video" | "audio";
+ url: string;
+ remote_url: string | null;
+ preview_url: string | null;
+ text_url: string | null;
+ meta: Meta | null;
+ description: string | null;
+ blurhash: string | null;
+ };
+}
diff --git a/packages/megalodon/src/entities/card.ts b/packages/megalodon/src/entities/card.ts
new file mode 100644
index 0000000000..356d99aee4
--- /dev/null
+++ b/packages/megalodon/src/entities/card.ts
@@ -0,0 +1,16 @@
+namespace Entity {
+ export type Card = {
+ url: string;
+ title: string;
+ description: string;
+ type: "link" | "photo" | "video" | "rich";
+ image?: string;
+ author_name?: string;
+ author_url?: string;
+ provider_name?: string;
+ provider_url?: string;
+ html?: string;
+ width?: number;
+ height?: number;
+ };
+}
diff --git a/packages/megalodon/src/entities/context.ts b/packages/megalodon/src/entities/context.ts
new file mode 100644
index 0000000000..a794a7c5a8
--- /dev/null
+++ b/packages/megalodon/src/entities/context.ts
@@ -0,0 +1,8 @@
+/// <reference path="status.ts" />
+
+namespace Entity {
+ export type Context = {
+ ancestors: Array<Status>;
+ descendants: Array<Status>;
+ };
+}
diff --git a/packages/megalodon/src/entities/conversation.ts b/packages/megalodon/src/entities/conversation.ts
new file mode 100644
index 0000000000..2bdc196661
--- /dev/null
+++ b/packages/megalodon/src/entities/conversation.ts
@@ -0,0 +1,11 @@
+/// <reference path="account.ts" />
+/// <reference path="status.ts" />
+
+namespace Entity {
+ export type Conversation = {
+ id: string;
+ accounts: Array<Account>;
+ last_status: Status | null;
+ unread: boolean;
+ };
+}
diff --git a/packages/megalodon/src/entities/emoji.ts b/packages/megalodon/src/entities/emoji.ts
new file mode 100644
index 0000000000..10c32ab0bd
--- /dev/null
+++ b/packages/megalodon/src/entities/emoji.ts
@@ -0,0 +1,9 @@
+namespace Entity {
+ export type Emoji = {
+ shortcode: string;
+ static_url: string;
+ url: string;
+ visible_in_picker: boolean;
+ category: string;
+ };
+}
diff --git a/packages/megalodon/src/entities/featured_tag.ts b/packages/megalodon/src/entities/featured_tag.ts
new file mode 100644
index 0000000000..fc9f8c69cc
--- /dev/null
+++ b/packages/megalodon/src/entities/featured_tag.ts
@@ -0,0 +1,8 @@
+namespace Entity {
+ export type FeaturedTag = {
+ id: string;
+ name: string;
+ statuses_count: number;
+ last_status_at: string;
+ };
+}
diff --git a/packages/megalodon/src/entities/field.ts b/packages/megalodon/src/entities/field.ts
new file mode 100644
index 0000000000..de4b6b2b72
--- /dev/null
+++ b/packages/megalodon/src/entities/field.ts
@@ -0,0 +1,7 @@
+namespace Entity {
+ export type Field = {
+ name: string;
+ value: string;
+ verified_at: string | null;
+ };
+}
diff --git a/packages/megalodon/src/entities/filter.ts b/packages/megalodon/src/entities/filter.ts
new file mode 100644
index 0000000000..55b7305cc3
--- /dev/null
+++ b/packages/megalodon/src/entities/filter.ts
@@ -0,0 +1,12 @@
+namespace Entity {
+ export type Filter = {
+ id: string;
+ phrase: string;
+ context: Array<FilterContext>;
+ expires_at: string | null;
+ irreversible: boolean;
+ whole_word: boolean;
+ };
+
+ export type FilterContext = string;
+}
diff --git a/packages/megalodon/src/entities/history.ts b/packages/megalodon/src/entities/history.ts
new file mode 100644
index 0000000000..4676357d69
--- /dev/null
+++ b/packages/megalodon/src/entities/history.ts
@@ -0,0 +1,7 @@
+namespace Entity {
+ export type History = {
+ day: string;
+ uses: number;
+ accounts: number;
+ };
+}
diff --git a/packages/megalodon/src/entities/identity_proof.ts b/packages/megalodon/src/entities/identity_proof.ts
new file mode 100644
index 0000000000..3b42e6f412
--- /dev/null
+++ b/packages/megalodon/src/entities/identity_proof.ts
@@ -0,0 +1,9 @@
+namespace Entity {
+ export type IdentityProof = {
+ provider: string;
+ provider_username: string;
+ updated_at: string;
+ proof_url: string;
+ profile_url: string;
+ };
+}
diff --git a/packages/megalodon/src/entities/instance.ts b/packages/megalodon/src/entities/instance.ts
new file mode 100644
index 0000000000..9c0f572db4
--- /dev/null
+++ b/packages/megalodon/src/entities/instance.ts
@@ -0,0 +1,41 @@
+/// <reference path="account.ts" />
+/// <reference path="urls.ts" />
+/// <reference path="stats.ts" />
+
+namespace Entity {
+ export type Instance = {
+ uri: string;
+ title: string;
+ description: string;
+ email: string;
+ version: string;
+ thumbnail: string | null;
+ urls: URLs;
+ stats: Stats;
+ languages: Array<string>;
+ contact_account: Account | null;
+ max_toot_chars?: number;
+ registrations?: boolean;
+ configuration?: {
+ statuses: {
+ max_characters: number;
+ max_media_attachments: number;
+ characters_reserved_per_url: number;
+ };
+ media_attachments: {
+ supported_mime_types: Array<string>;
+ image_size_limit: number;
+ image_matrix_limit: number;
+ video_size_limit: number;
+ video_frame_limit: number;
+ video_matrix_limit: number;
+ };
+ polls: {
+ max_options: number;
+ max_characters_per_option: number;
+ min_expiration: number;
+ max_expiration: number;
+ };
+ };
+ };
+}
diff --git a/packages/megalodon/src/entities/list.ts b/packages/megalodon/src/entities/list.ts
new file mode 100644
index 0000000000..97e75286b2
--- /dev/null
+++ b/packages/megalodon/src/entities/list.ts
@@ -0,0 +1,6 @@
+namespace Entity {
+ export type List = {
+ id: string;
+ title: string;
+ };
+}
diff --git a/packages/megalodon/src/entities/marker.ts b/packages/megalodon/src/entities/marker.ts
new file mode 100644
index 0000000000..7ee99282ca
--- /dev/null
+++ b/packages/megalodon/src/entities/marker.ts
@@ -0,0 +1,15 @@
+namespace Entity {
+ export type Marker = {
+ home?: {
+ last_read_id: string;
+ version: number;
+ updated_at: string;
+ };
+ notifications?: {
+ last_read_id: string;
+ version: number;
+ updated_at: string;
+ unread_count?: number;
+ };
+ };
+}
diff --git a/packages/megalodon/src/entities/mention.ts b/packages/megalodon/src/entities/mention.ts
new file mode 100644
index 0000000000..4fe36a6553
--- /dev/null
+++ b/packages/megalodon/src/entities/mention.ts
@@ -0,0 +1,8 @@
+namespace Entity {
+ export type Mention = {
+ id: string;
+ username: string;
+ url: string;
+ acct: string;
+ };
+}
diff --git a/packages/megalodon/src/entities/notification.ts b/packages/megalodon/src/entities/notification.ts
new file mode 100644
index 0000000000..68eff3347e
--- /dev/null
+++ b/packages/megalodon/src/entities/notification.ts
@@ -0,0 +1,15 @@
+/// <reference path="account.ts" />
+/// <reference path="status.ts" />
+
+namespace Entity {
+ export type Notification = {
+ account: Account;
+ created_at: string;
+ id: string;
+ status?: Status;
+ reaction?: Reaction;
+ type: NotificationType;
+ };
+
+ export type NotificationType = string;
+}
diff --git a/packages/megalodon/src/entities/poll.ts b/packages/megalodon/src/entities/poll.ts
new file mode 100644
index 0000000000..2539d68b20
--- /dev/null
+++ b/packages/megalodon/src/entities/poll.ts
@@ -0,0 +1,14 @@
+/// <reference path="poll_option.ts" />
+
+namespace Entity {
+ export type Poll = {
+ id: string;
+ expires_at: string | null;
+ expired: boolean;
+ multiple: boolean;
+ votes_count: number;
+ options: Array<PollOption>;
+ voted: boolean;
+ own_votes: Array<number>;
+ };
+}
diff --git a/packages/megalodon/src/entities/poll_option.ts b/packages/megalodon/src/entities/poll_option.ts
new file mode 100644
index 0000000000..e818a8607b
--- /dev/null
+++ b/packages/megalodon/src/entities/poll_option.ts
@@ -0,0 +1,6 @@
+namespace Entity {
+ export type PollOption = {
+ title: string;
+ votes_count: number | null;
+ };
+}
diff --git a/packages/megalodon/src/entities/preferences.ts b/packages/megalodon/src/entities/preferences.ts
new file mode 100644
index 0000000000..7994dc568e
--- /dev/null
+++ b/packages/megalodon/src/entities/preferences.ts
@@ -0,0 +1,9 @@
+namespace Entity {
+ export type Preferences = {
+ "posting:default:visibility": "public" | "unlisted" | "private" | "direct";
+ "posting:default:sensitive": boolean;
+ "posting:default:language": string | null;
+ "reading:expand:media": "default" | "show_all" | "hide_all";
+ "reading:expand:spoilers": boolean;
+ };
+}
diff --git a/packages/megalodon/src/entities/push_subscription.ts b/packages/megalodon/src/entities/push_subscription.ts
new file mode 100644
index 0000000000..ad1146a242
--- /dev/null
+++ b/packages/megalodon/src/entities/push_subscription.ts
@@ -0,0 +1,16 @@
+namespace Entity {
+ export type Alerts = {
+ follow: boolean;
+ favourite: boolean;
+ mention: boolean;
+ reblog: boolean;
+ poll: boolean;
+ };
+
+ export type PushSubscription = {
+ id: string;
+ endpoint: string;
+ server_key: string;
+ alerts: Alerts;
+ };
+}
diff --git a/packages/megalodon/src/entities/reaction.ts b/packages/megalodon/src/entities/reaction.ts
new file mode 100644
index 0000000000..4edbec6a7d
--- /dev/null
+++ b/packages/megalodon/src/entities/reaction.ts
@@ -0,0 +1,12 @@
+/// <reference path="account.ts" />
+
+namespace Entity {
+ export type Reaction = {
+ count: number;
+ me: boolean;
+ name: string;
+ url?: string;
+ static_url?: string;
+ accounts?: Array<Account>;
+ };
+}
diff --git a/packages/megalodon/src/entities/relationship.ts b/packages/megalodon/src/entities/relationship.ts
new file mode 100644
index 0000000000..91802d5c88
--- /dev/null
+++ b/packages/megalodon/src/entities/relationship.ts
@@ -0,0 +1,17 @@
+namespace Entity {
+ export type Relationship = {
+ id: string;
+ following: boolean;
+ followed_by: boolean;
+ delivery_following?: boolean;
+ blocking: boolean;
+ blocked_by: boolean;
+ muting: boolean;
+ muting_notifications: boolean;
+ requested: boolean;
+ domain_blocking: boolean;
+ showing_reblogs: boolean;
+ endorsed: boolean;
+ notifying: boolean;
+ };
+}
diff --git a/packages/megalodon/src/entities/report.ts b/packages/megalodon/src/entities/report.ts
new file mode 100644
index 0000000000..6862a5fabe
--- /dev/null
+++ b/packages/megalodon/src/entities/report.ts
@@ -0,0 +1,9 @@
+namespace Entity {
+ export type Report = {
+ id: string;
+ action_taken: string;
+ comment: string;
+ account_id: string;
+ status_ids: Array<string>;
+ };
+}
diff --git a/packages/megalodon/src/entities/results.ts b/packages/megalodon/src/entities/results.ts
new file mode 100644
index 0000000000..4448e53350
--- /dev/null
+++ b/packages/megalodon/src/entities/results.ts
@@ -0,0 +1,11 @@
+/// <reference path="account.ts" />
+/// <reference path="status.ts" />
+/// <reference path="tag.ts" />
+
+namespace Entity {
+ export type Results = {
+ accounts: Array<Account>;
+ statuses: Array<Status>;
+ hashtags: Array<Tag>;
+ };
+}
diff --git a/packages/megalodon/src/entities/scheduled_status.ts b/packages/megalodon/src/entities/scheduled_status.ts
new file mode 100644
index 0000000000..78dfb8ed26
--- /dev/null
+++ b/packages/megalodon/src/entities/scheduled_status.ts
@@ -0,0 +1,10 @@
+/// <reference path="attachment.ts" />
+/// <reference path="status_params.ts" />
+namespace Entity {
+ export type ScheduledStatus = {
+ id: string;
+ scheduled_at: string;
+ params: StatusParams;
+ media_attachments: Array<Attachment>;
+ };
+}
diff --git a/packages/megalodon/src/entities/source.ts b/packages/megalodon/src/entities/source.ts
new file mode 100644
index 0000000000..913b02fda7
--- /dev/null
+++ b/packages/megalodon/src/entities/source.ts
@@ -0,0 +1,10 @@
+/// <reference path="field.ts" />
+namespace Entity {
+ export type Source = {
+ privacy: string | null;
+ sensitive: boolean | null;
+ language: string | null;
+ note: string;
+ fields: Array<Field>;
+ };
+}
diff --git a/packages/megalodon/src/entities/stats.ts b/packages/megalodon/src/entities/stats.ts
new file mode 100644
index 0000000000..6471df039a
--- /dev/null
+++ b/packages/megalodon/src/entities/stats.ts
@@ -0,0 +1,7 @@
+namespace Entity {
+ export type Stats = {
+ user_count: number;
+ status_count: number;
+ domain_count: number;
+ };
+}
diff --git a/packages/megalodon/src/entities/status.ts b/packages/megalodon/src/entities/status.ts
new file mode 100644
index 0000000000..f27f728b54
--- /dev/null
+++ b/packages/megalodon/src/entities/status.ts
@@ -0,0 +1,45 @@
+/// <reference path="account.ts" />
+/// <reference path="application.ts" />
+/// <reference path="mention.ts" />
+/// <reference path="tag.ts" />
+/// <reference path="attachment.ts" />
+/// <reference path="emoji.ts" />
+/// <reference path="card.ts" />
+/// <reference path="poll.ts" />
+/// <reference path="reaction.ts" />
+
+namespace Entity {
+ export type Status = {
+ id: string;
+ uri: string;
+ url: string;
+ account: Account;
+ in_reply_to_id: string | null;
+ in_reply_to_account_id: string | null;
+ reblog: Status | null;
+ content: string;
+ plain_content: string | null;
+ created_at: string;
+ emojis: Emoji[];
+ replies_count: number;
+ reblogs_count: number;
+ favourites_count: number;
+ reblogged: boolean | null;
+ favourited: boolean | null;
+ muted: boolean | null;
+ sensitive: boolean;
+ spoiler_text: string;
+ visibility: "public" | "unlisted" | "private" | "direct";
+ media_attachments: Array<Attachment>;
+ mentions: Array<Mention>;
+ tags: Array<Tag>;
+ card: Card | null;
+ poll: Poll | null;
+ application: Application | null;
+ language: string | null;
+ pinned: boolean | null;
+ reactions: Array<Reaction>;
+ quote: Status | null;
+ bookmarked: boolean;
+ };
+}
diff --git a/packages/megalodon/src/entities/status_edit.ts b/packages/megalodon/src/entities/status_edit.ts
new file mode 100644
index 0000000000..4040b4ff90
--- /dev/null
+++ b/packages/megalodon/src/entities/status_edit.ts
@@ -0,0 +1,23 @@
+/// <reference path="account.ts" />
+/// <reference path="application.ts" />
+/// <reference path="mention.ts" />
+/// <reference path="tag.ts" />
+/// <reference path="attachment.ts" />
+/// <reference path="emoji.ts" />
+/// <reference path="card.ts" />
+/// <reference path="poll.ts" />
+/// <reference path="reaction.ts" />
+
+namespace Entity {
+ export type StatusEdit = {
+ account: Account;
+ content: string;
+ plain_content: string | null;
+ created_at: string;
+ emojis: Emoji[];
+ sensitive: boolean;
+ spoiler_text: string;
+ media_attachments: Array<Attachment>;
+ poll: Poll | null;
+ };
+}
diff --git a/packages/megalodon/src/entities/status_params.ts b/packages/megalodon/src/entities/status_params.ts
new file mode 100644
index 0000000000..18908c01c1
--- /dev/null
+++ b/packages/megalodon/src/entities/status_params.ts
@@ -0,0 +1,12 @@
+namespace Entity {
+ export type StatusParams = {
+ text: string;
+ in_reply_to_id: string | null;
+ media_ids: Array<string> | null;
+ sensitive: boolean | null;
+ spoiler_text: string | null;
+ visibility: "public" | "unlisted" | "private" | "direct";
+ scheduled_at: string | null;
+ application_id: string;
+ };
+}
diff --git a/packages/megalodon/src/entities/tag.ts b/packages/megalodon/src/entities/tag.ts
new file mode 100644
index 0000000000..ccc88aece6
--- /dev/null
+++ b/packages/megalodon/src/entities/tag.ts
@@ -0,0 +1,10 @@
+/// <reference path="history.ts" />
+
+namespace Entity {
+ export type Tag = {
+ name: string;
+ url: string;
+ history: Array<History> | null;
+ following?: boolean;
+ };
+}
diff --git a/packages/megalodon/src/entities/token.ts b/packages/megalodon/src/entities/token.ts
new file mode 100644
index 0000000000..1583edafb1
--- /dev/null
+++ b/packages/megalodon/src/entities/token.ts
@@ -0,0 +1,8 @@
+namespace Entity {
+ export type Token = {
+ access_token: string;
+ token_type: string;
+ scope: string;
+ created_at: number;
+ };
+}
diff --git a/packages/megalodon/src/entities/urls.ts b/packages/megalodon/src/entities/urls.ts
new file mode 100644
index 0000000000..1ee9ed67c9
--- /dev/null
+++ b/packages/megalodon/src/entities/urls.ts
@@ -0,0 +1,5 @@
+namespace Entity {
+ export type URLs = {
+ streaming_api: string;
+ };
+}
diff --git a/packages/megalodon/src/entity.ts b/packages/megalodon/src/entity.ts
new file mode 100644
index 0000000000..b73d2b359b
--- /dev/null
+++ b/packages/megalodon/src/entity.ts
@@ -0,0 +1,38 @@
+/// <reference path="./entities/account.ts" />
+/// <reference path="./entities/activity.ts" />
+/// <reference path="./entities/announcement.ts" />
+/// <reference path="./entities/application.ts" />
+/// <reference path="./entities/async_attachment.ts" />
+/// <reference path="./entities/attachment.ts" />
+/// <reference path="./entities/card.ts" />
+/// <reference path="./entities/context.ts" />
+/// <reference path="./entities/conversation.ts" />
+/// <reference path="./entities/emoji.ts" />
+/// <reference path="./entities/featured_tag.ts" />
+/// <reference path="./entities/field.ts" />
+/// <reference path="./entities/filter.ts" />
+/// <reference path="./entities/history.ts" />
+/// <reference path="./entities/identity_proof.ts" />
+/// <reference path="./entities/instance.ts" />
+/// <reference path="./entities/list.ts" />
+/// <reference path="./entities/marker.ts" />
+/// <reference path="./entities/mention.ts" />
+/// <reference path="./entities/notification.ts" />
+/// <reference path="./entities/poll.ts" />
+/// <reference path="./entities/poll_option.ts" />
+/// <reference path="./entities/preferences.ts" />
+/// <reference path="./entities/push_subscription.ts" />
+/// <reference path="./entities/reaction.ts" />
+/// <reference path="./entities/relationship.ts" />
+/// <reference path="./entities/report.ts" />
+/// <reference path="./entities/results.ts" />
+/// <reference path="./entities/scheduled_status.ts" />
+/// <reference path="./entities/source.ts" />
+/// <reference path="./entities/stats.ts" />
+/// <reference path="./entities/status.ts" />
+/// <reference path="./entities/status_params.ts" />
+/// <reference path="./entities/tag.ts" />
+/// <reference path="./entities/token.ts" />
+/// <reference path="./entities/urls.ts" />
+
+export default Entity;
diff --git a/packages/megalodon/src/filter_context.ts b/packages/megalodon/src/filter_context.ts
new file mode 100644
index 0000000000..4c83cb15f2
--- /dev/null
+++ b/packages/megalodon/src/filter_context.ts
@@ -0,0 +1,11 @@
+import Entity from "./entity";
+
+namespace FilterContext {
+ export const Home: Entity.FilterContext = "home";
+ export const Notifications: Entity.FilterContext = "notifications";
+ export const Public: Entity.FilterContext = "public";
+ export const Thread: Entity.FilterContext = "thread";
+ export const Account: Entity.FilterContext = "account";
+}
+
+export default FilterContext;
diff --git a/packages/megalodon/src/index.ts b/packages/megalodon/src/index.ts
new file mode 100644
index 0000000000..758d3a46ad
--- /dev/null
+++ b/packages/megalodon/src/index.ts
@@ -0,0 +1,32 @@
+import Response from "./response";
+import OAuth from "./oauth";
+import { isCancel, RequestCanceledError } from "./cancel";
+import { ProxyConfig } from "./proxy_config";
+import generator, {
+ detector,
+ MegalodonInterface,
+ WebSocketInterface,
+} from "./megalodon";
+import Misskey from "./misskey";
+import Entity from "./entity";
+import NotificationType from "./notification";
+import FilterContext from "./filter_context";
+import Converter from "./converter";
+
+export {
+ Response,
+ OAuth,
+ RequestCanceledError,
+ isCancel,
+ ProxyConfig,
+ detector,
+ MegalodonInterface,
+ WebSocketInterface,
+ NotificationType,
+ FilterContext,
+ Misskey,
+ Entity,
+ Converter,
+};
+
+export default generator;
diff --git a/packages/megalodon/src/megalodon.ts b/packages/megalodon/src/megalodon.ts
new file mode 100644
index 0000000000..33a5790f67
--- /dev/null
+++ b/packages/megalodon/src/megalodon.ts
@@ -0,0 +1,1532 @@
+import Response from "./response";
+import OAuth from "./oauth";
+import proxyAgent, { ProxyConfig } from "./proxy_config";
+import Entity from "./entity";
+import axios, { AxiosRequestConfig } from "axios";
+import Misskey from "./misskey";
+import { DEFAULT_UA } from "./default";
+
+export interface WebSocketInterface {
+ start(): void;
+ stop(): void;
+ // EventEmitter
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
+ once(event: string | symbol, listener: (...args: any[]) => void): this;
+ removeListener(
+ event: string | symbol,
+ listener: (...args: any[]) => void,
+ ): this;
+ removeAllListeners(event?: string | symbol): this;
+}
+
+export interface MegalodonInterface {
+ /**
+ * Cancel all requests in this instance.
+ *
+ * @return void
+ */
+ cancel(): void;
+
+ /**
+ * First, call createApp to get client_id and client_secret.
+ * Next, call generateAuthUrl to get authorization url.
+ * @param client_name Form Data, which is sent to /api/v1/apps
+ * @param options Form Data, which is sent to /api/v1/apps. and properties should be **snake_case**
+ */
+ registerApp(
+ client_name: string,
+ options: Partial<{
+ scopes: Array<string>;
+ redirect_uris: string;
+ website: string;
+ }>,
+ ): Promise<OAuth.AppData>;
+
+ /**
+ * Call /api/v1/apps
+ *
+ * Create an application.
+ * @param client_name your application's name
+ * @param options Form Data
+ */
+ createApp(
+ client_name: string,
+ options: Partial<{
+ scopes: Array<string>;
+ redirect_uris: string;
+ website: string;
+ }>,
+ ): Promise<OAuth.AppData>;
+
+ // ======================================
+ // apps
+ // ======================================
+ /**
+ * GET /api/v1/apps/verify_credentials
+ *
+ * @return An Application
+ */
+ verifyAppCredentials(): Promise<Response<Entity.Application>>;
+
+ // ======================================
+ // apps/oauth
+ // ======================================
+
+ /**
+ * POST /oauth/token
+ *
+ * Fetch OAuth access token.
+ * Get an access token based client_id and client_secret and authorization code.
+ * @param client_id will be generated by #createApp or #registerApp
+ * @param client_secret will be generated by #createApp or #registerApp
+ * @param code will be generated by the link of #generateAuthUrl or #registerApp
+ * @param redirect_uri must be the same uri as the time when you register your OAuth application
+ */
+ fetchAccessToken(
+ client_id: string | null,
+ client_secret: string,
+ code: string,
+ redirect_uri?: string,
+ ): Promise<OAuth.TokenData>;
+
+ /**
+ * POST /oauth/token
+ *
+ * Refresh OAuth access token.
+ * Send refresh token and get new access token.
+ * @param client_id will be generated by #createApp or #registerApp
+ * @param client_secret will be generated by #createApp or #registerApp
+ * @param refresh_token will be get #fetchAccessToken
+ */
+ refreshToken(
+ client_id: string,
+ client_secret: string,
+ refresh_token: string,
+ ): Promise<OAuth.TokenData>;
+
+ /**
+ * POST /oauth/revoke
+ *
+ * Revoke an OAuth token.
+ * @param client_id will be generated by #createApp or #registerApp
+ * @param client_secret will be generated by #createApp or #registerApp
+ * @param token will be get #fetchAccessToken
+ */
+ revokeToken(
+ client_id: string,
+ client_secret: string,
+ token: string,
+ ): Promise<Response<{}>>;
+
+ // ======================================
+ // accounts
+ // ======================================
+ /**
+ * POST /api/v1/accounts
+ *
+ * @param username Username for the account.
+ * @param email Email for the account.
+ * @param password Password for the account.
+ * @param agreement Whether the user agrees to the local rules, terms, and policies.
+ * @param locale The language of the confirmation email that will be sent
+ * @param reason Text that will be reviewed by moderators if registrations require manual approval.
+ * @return An account token.
+ */
+ registerAccount(
+ username: string,
+ email: string,
+ password: string,
+ agreement: boolean,
+ locale: string,
+ reason?: string | null,
+ ): Promise<Response<Entity.Token>>;
+ /**
+ * GET /api/v1/accounts/verify_credentials
+ *
+ * @return Account.
+ */
+ verifyAccountCredentials(): Promise<Response<Entity.Account>>;
+ /**
+ * PATCH /api/v1/accounts/update_credentials
+ *
+ * @return An account.
+ */
+ updateCredentials(options?: {
+ discoverable?: boolean;
+ bot?: boolean;
+ display_name?: string;
+ note?: string;
+ avatar?: string;
+ header?: string;
+ locked?: boolean;
+ source?: {
+ privacy?: string;
+ sensitive?: boolean;
+ language?: string;
+ };
+ fields_attributes?: Array<{ name: string; value: string }>;
+ }): Promise<Response<Entity.Account>>;
+ /**
+ * GET /api/v1/accounts/:id
+ *
+ * @param id The account ID.
+ * @return An account.
+ */
+ getAccount(id: string): Promise<Response<Entity.Account>>;
+ /**
+ * GET /api/v1/accounts/:id/statuses
+ *
+ * @param id The account ID.
+
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID but starting with most recent.
+ * @param options.min_id Return results newer than ID.
+ * @param options.pinned Return statuses which include pinned statuses.
+ * @param options.exclude_replies Return statuses which exclude replies.
+ * @param options.exclude_reblogs Return statuses which exclude reblogs.
+ * @param options.only_media Show only statuses with media attached? Defaults to false.
+ * @return Account's statuses.
+ */
+ getAccountStatuses(
+ id: string,
+ options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ pinned?: boolean;
+ exclude_replies?: boolean;
+ exclude_reblogs?: boolean;
+ only_media?: boolean;
+ },
+ ): Promise<Response<Array<Entity.Status>>>;
+ /**
+ * GET /api/v1/pleroma/accounts/:id/favourites
+ *
+ * @param id Target account ID.
+ * @param options.limit Max number of results to return.
+ * @param options.max_id Return results order than ID.
+ * @param options.since_id Return results newer than ID.
+ * @return Array of statuses.
+ */
+ getAccountFavourites(
+ id: string,
+ options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ },
+ ): Promise<Response<Array<Entity.Status>>>;
+ /**
+ * POST /api/v1/pleroma/accounts/:id/subscribe
+ *
+ * @param id Target account ID.
+ * @return Relationship.
+ */
+ subscribeAccount(id: string): Promise<Response<Entity.Relationship>>;
+ /**
+ * POST /api/v1/pleroma/accounts/:id/unsubscribe
+ *
+ * @param id Target account ID.
+ * @return Relationship.
+ */
+ unsubscribeAccount(id: string): Promise<Response<Entity.Relationship>>;
+ /**
+ * GET /api/v1/accounts/:id/followers
+ *
+ * @param id The account ID.
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @return The array of accounts.
+ */
+ getAccountFollowers(
+ id: string,
+ options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ get_all?: boolean;
+ sleep_ms?: number;
+ },
+ ): Promise<Response<Array<Entity.Account>>>;
+
+ /**
+ * GET /api/v1/accounts/:id/featured_tags
+ *
+ * @param id The account ID.
+ * @return The array of accounts.
+ */
+ getAccountFeaturedTags(
+ id: string,
+ ): Promise<Response<Array<Entity.FeaturedTag>>>;
+
+ /**
+ * GET /api/v1/accounts/:id/following
+ *
+ * @param id The account ID.
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @return The array of accounts.
+ */
+ getAccountFollowing(
+ id: string,
+ options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ get_all?: boolean;
+ sleep_ms?: number;
+ },
+ ): Promise<Response<Array<Entity.Account>>>;
+ /**
+ * GET /api/v1/accounts/:id/lists
+ *
+ * @param id The account ID.
+ * @return The array of lists.
+ */
+ getAccountLists(id: string): Promise<Response<Array<Entity.List>>>;
+ /**
+ * GET /api/v1/accounts/:id/identity_proofs
+ *
+ * @param id The account ID.
+ * @return Array of IdentityProof
+ */
+ getIdentityProof(id: string): Promise<Response<Array<Entity.IdentityProof>>>;
+ /**
+ * POST /api/v1/accounts/:id/follow
+ *
+ * @param id The account ID.
+ * @param reblog Receive this account's reblogs in home timeline.
+ * @return Relationship
+ */
+ followAccount(
+ id: string,
+ options?: {
+ reblog?: boolean;
+ },
+ ): Promise<Response<Entity.Relationship>>;
+ /**
+ * POST /api/v1/accounts/:id/unfollow
+ *
+ * @param id The account ID.
+ * @return Relationship
+ */
+ unfollowAccount(id: string): Promise<Response<Entity.Relationship>>;
+ /**
+ * POST /api/v1/accounts/:id/block
+ *
+ * @param id The account ID.
+ * @return Relationship
+ */
+ blockAccount(id: string): Promise<Response<Entity.Relationship>>;
+ /**
+ * POST /api/v1/accounts/:id/unblock
+ *
+ * @param id The account ID.
+ * @return RElationship
+ */
+ unblockAccount(id: string): Promise<Response<Entity.Relationship>>;
+ /**
+ * POST /api/v1/accounts/:id/mute
+ *
+ * @param id The account ID.
+ * @param notifications Mute notifications in addition to statuses.
+ * @return Relationship
+ */
+ muteAccount(
+ id: string,
+ notifications: boolean,
+ ): Promise<Response<Entity.Relationship>>;
+ /**
+ * POST /api/v1/accounts/:id/unmute
+ *
+ * @param id The account ID.
+ * @return Relationship
+ */
+ unmuteAccount(id: string): Promise<Response<Entity.Relationship>>;
+ /**
+ * POST /api/v1/accounts/:id/pin
+ *
+ * @param id The account ID.
+ * @return Relationship
+ */
+ pinAccount(id: string): Promise<Response<Entity.Relationship>>;
+ /**
+ * POST /api/v1/accounts/:id/unpin
+ *
+ * @param id The account ID.
+ * @return Relationship
+ */
+ unpinAccount(id: string): Promise<Response<Entity.Relationship>>;
+ /**
+ * GET /api/v1/accounts/relationships
+ *
+ * @param id The account ID.
+ * @return Relationship
+ */
+ getRelationship(id: string): Promise<Response<Entity.Relationship>>;
+ /**
+ * Get multiple relationships in one method
+ *
+ * @param ids Array of account IDs.
+ * @return Array of Relationship.
+ */
+ getRelationships(
+ ids: Array<string>,
+ ): Promise<Response<Array<Entity.Relationship>>>;
+ /**
+ * GET /api/v1/accounts/search
+ *
+ * @param q Search query.
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @return The array of accounts.
+ */
+ searchAccount(
+ q: string,
+ options?: {
+ following?: boolean;
+ resolve?: boolean;
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ },
+ ): Promise<Response<Array<Entity.Account>>>;
+ // ======================================
+ // accounts/bookmarks
+ // ======================================
+ /**
+ * GET /api/v1/bookmarks
+ *
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ getBookmarks(options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Status>>>;
+ // ======================================
+ // accounts/favourites
+ // ======================================
+ /**
+ * GET /api/v1/favourites
+ *
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ getFavourites(options?: {
+ limit?: number;
+ max_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Status>>>;
+ // ======================================
+ // accounts/mutes
+ // ======================================
+ /**
+ * GET /api/v1/mutes
+ *
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of accounts.
+ */
+ getMutes(options?: {
+ limit?: number;
+ max_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Account>>>;
+ // ======================================
+ // accounts/blocks
+ // ======================================
+ /**
+ * GET /api/v1/blocks
+ *
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of accounts.
+ */
+ getBlocks(options?: {
+ limit?: number;
+ max_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Account>>>;
+ // ======================================
+ // accounts/domain_blocks
+ // ======================================
+ /**
+ * GET /api/v1/domain_blocks
+ *
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of domain name.
+ */
+ getDomainBlocks(options?: {
+ limit?: number;
+ max_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<string>>>;
+ /**
+ * POST/api/v1/domain_blocks
+ *
+ * @param domain Domain to block.
+ */
+ blockDomain(domain: string): Promise<Response<{}>>;
+ /**
+ * DELETE /api/v1/domain_blocks
+ *
+ * @param domain Domain to unblock
+ */
+ unblockDomain(domain: string): Promise<Response<{}>>;
+ // ======================================
+ // accounts/filters
+ // ======================================
+ /**
+ * GET /api/v1/filters
+ *
+ * @return Array of filters.
+ */
+ getFilters(): Promise<Response<Array<Entity.Filter>>>;
+ /**
+ * GET /api/v1/filters/:id
+ *
+ * @param id The filter ID.
+ * @return Filter.
+ */
+ getFilter(id: string): Promise<Response<Entity.Filter>>;
+ /**
+ * POST /api/v1/filters
+ *
+ * @param phrase Text to be filtered.
+ * @param context Array of enumerable strings home, notifications, public, thread, account. At least one context must be specified.
+ * @param options.irreversible Should the server irreversibly drop matching entities from home and notifications?
+ * @param options.whole_word Consider word boundaries?
+ * @param options.expires_in ISO 8601 Datetime for when the filter expires.
+ * @return Filter
+ */
+ createFilter(
+ phrase: string,
+ context: Array<Entity.FilterContext>,
+ options?: {
+ irreversible?: boolean;
+ whole_word?: boolean;
+ expires_in?: string;
+ },
+ ): Promise<Response<Entity.Filter>>;
+ /**
+ * PUT /api/v1/filters/:id
+ *
+ * @param id The filter ID.
+ * @param phrase Text to be filtered.
+ * @param context Array of enumerable strings home, notifications, public, thread, account. At least one context must be specified.
+ * @param options.irreversible Should the server irreversibly drop matching entities from home and notifications?
+ * @param options.whole_word Consider word boundaries?
+ * @param options.expires_in ISO 8601 Datetime for when the filter expires.
+ * @return Filter
+ */
+ updateFilter(
+ id: string,
+ phrase: string,
+ context: Array<Entity.FilterContext>,
+ options?: {
+ irreversible?: boolean;
+ whole_word?: boolean;
+ expires_in?: string;
+ },
+ ): Promise<Response<Entity.Filter>>;
+ /**
+ * DELETE /api/v1/filters/:id
+ *
+ * @param id The filter ID.
+ * @return Removed filter.
+ */
+ deleteFilter(id: string): Promise<Response<Entity.Filter>>;
+ // ======================================
+ // accounts/reports
+ // ======================================
+ /**
+ * POST /api/v1/reports
+ *
+ * @param account_id Target account ID.
+ * @param comment Reason of the report.
+ * @param options.status_ids Array of Statuses ids to attach to the report.
+ * @param options.forward If the account is remote, should the report be forwarded to the remote admin?
+ * @return Report
+ */
+ report(
+ account_id: string,
+ comment: string,
+ options?: { status_ids?: Array<string>; forward?: boolean },
+ ): Promise<Response<Entity.Report>>;
+ // ======================================
+ // accounts/follow_requests
+ // ======================================
+ /**
+ * GET /api/v1/follow_requests
+ *
+ * @param limit Maximum number of results.
+ * @return Array of account.
+ */
+ getFollowRequests(limit?: number): Promise<Response<Array<Entity.Account>>>;
+ /**
+ * POST /api/v1/follow_requests/:id/authorize
+ *
+ * @param id Target account ID.
+ * @return Relationship.
+ */
+ acceptFollowRequest(id: string): Promise<Response<Entity.Relationship>>;
+ /**
+ * POST /api/v1/follow_requests/:id/reject
+ *
+ * @param id Target account ID.
+ * @return Relationship.
+ */
+ rejectFollowRequest(id: string): Promise<Response<Entity.Relationship>>;
+ // ======================================
+ // accounts/endorsements
+ // ======================================
+ /**
+ * GET /api/v1/endorsements
+ *
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @return Array of accounts.
+ */
+ getEndorsements(options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ }): Promise<Response<Array<Entity.Account>>>;
+ // ======================================
+ // accounts/featured_tags
+ // ======================================
+ /**
+ * GET /api/v1/featured_tags
+ *
+ * @return Array of featured tag.
+ */
+ getFeaturedTags(): Promise<Response<Array<Entity.FeaturedTag>>>;
+ /**
+ * POST /api/v1/featured_tags
+ *
+ * @param name Target hashtag name.
+ * @return FeaturedTag.
+ */
+ createFeaturedTag(name: string): Promise<Response<Entity.FeaturedTag>>;
+ /**
+ * DELETE /api/v1/featured_tags/:id
+ *
+ * @param id Target featured tag id.
+ * @return Empty
+ */
+ deleteFeaturedTag(id: string): Promise<Response<{}>>;
+ /**
+ * GET /api/v1/featured_tags/suggestions
+ *
+ * @return Array of tag.
+ */
+ getSuggestedTags(): Promise<Response<Array<Entity.Tag>>>;
+ // ======================================
+ // accounts/preferences
+ // ======================================
+ /**
+ * GET /api/v1/preferences
+ *
+ * @return Preferences.
+ */
+ getPreferences(): Promise<Response<Entity.Preferences>>;
+ // ======================================
+ // accounts/suggestions
+ // ======================================
+ /**
+ * GET /api/v1/suggestions
+ *
+ * @param limit Maximum number of results.
+ * @return Array of accounts.
+ */
+ getSuggestions(limit?: number): Promise<Response<Array<Entity.Account>>>;
+ // ======================================
+ // accounts/tags
+ // ======================================
+ getFollowedTags(): Promise<Response<Array<Entity.Tag>>>;
+ /**
+ * GET /api/v1/tags/:id
+ *
+ * @param id Target hashtag id.
+ * @return Tag
+ */
+ getTag(id: string): Promise<Response<Entity.Tag>>;
+ /**
+ * POST /api/v1/tags/:id/follow
+ *
+ * @param id Target hashtag id.
+ * @return Tag
+ */
+ followTag(id: string): Promise<Response<Entity.Tag>>;
+ /**
+ * POST /api/v1/tags/:id/unfollow
+ *
+ * @param id Target hashtag id.
+ * @return Tag
+ */
+ unfollowTag(id: string): Promise<Response<Entity.Tag>>;
+ // ======================================
+ // statuses
+ // ======================================
+ /**
+ * POST /api/v1/statuses
+ *
+ * @param status Text content of status.
+ * @param options.media_ids Array of Attachment ids.
+ * @param options.poll Poll object.
+ * @param options.in_reply_to_id ID of the status being replied to, if status is a reply.
+ * @param options.sensitive Mark status and attached media as sensitive?
+ * @param options.spoiler_text Text to be shown as a warning or subject before the actual content.
+ * @param options.visibility Visibility of the posted status.
+ * @param options.scheduled_at ISO 8601 Datetime at which to schedule a status.
+ * @param options.language ISO 639 language code for this status.
+ * @param options.quote_id ID of the status being quoted to, if status is a quote.
+ * @return Status
+ */
+ postStatus(
+ status: string,
+ options?: {
+ media_ids?: Array<string>;
+ poll?: {
+ options: Array<string>;
+ expires_in: number;
+ multiple?: boolean;
+ hide_totals?: boolean;
+ };
+ in_reply_to_id?: string;
+ sensitive?: boolean;
+ spoiler_text?: string;
+ visibility?: "public" | "unlisted" | "private" | "direct";
+ scheduled_at?: string;
+ language?: string;
+ quote_id?: string;
+ },
+ ): Promise<Response<Entity.Status>>;
+ /**
+ * GET /api/v1/statuses/:id
+ *
+ * @param id The target status id.
+ * @return Status
+ */
+ getStatus(id: string): Promise<Response<Entity.Status>>;
+ /**
+ PUT /api/v1/statuses/:id
+ *
+ * @param id The target status id.
+ * @return Status
+ */
+ editStatus(
+ id: string,
+ options: {
+ status?: string;
+ spoiler_text?: string;
+ sensitive?: boolean;
+ media_ids?: Array<string>;
+ poll?: {
+ options?: Array<string>;
+ expires_in?: number;
+ multiple?: boolean;
+ hide_totals?: boolean;
+ };
+ },
+ ): Promise<Response<Entity.Status>>;
+ /**
+ * DELETE /api/v1/statuses/:id
+ *
+ * @param id The target status id.
+ * @return Status
+ */
+ deleteStatus(id: string): Promise<Response<{}>>;
+ /**
+ * GET /api/v1/statuses/:id/context
+ *
+ * Get parent and child statuses.
+ * @param id The target status id.
+ * @return Context
+ */
+ getStatusContext(
+ id: string,
+ options?: { limit?: number; max_id?: string; since_id?: string },
+ ): Promise<Response<Entity.Context>>;
+ /**
+ * GET /api/v1/statuses/:id/history
+ *
+ * Get status edit history.
+ * @param id The target status id.
+ * @return StatusEdit
+ */
+ getStatusHistory(id: string): Promise<Response<Array<Entity.StatusEdit>>>;
+ /**
+ * GET /api/v1/statuses/:id/reblogged_by
+ *
+ * @param id The target status id.
+ * @return Array of accounts.
+ */
+ getStatusRebloggedBy(id: string): Promise<Response<Array<Entity.Account>>>;
+ /**
+ * GET /api/v1/statuses/:id/favourited_by
+ *
+ * @param id The target status id.
+ * @return Array of accounts.
+ */
+ getStatusFavouritedBy(id: string): Promise<Response<Array<Entity.Account>>>;
+ /**
+ * POST /api/v1/statuses/:id/favourite
+ *
+ * @param id The target status id.
+ * @return Status.
+ */
+ favouriteStatus(id: string): Promise<Response<Entity.Status>>;
+ /**
+ * POST /api/v1/statuses/:id/unfavourite
+ *
+ * @param id The target status id.
+ * @return Status.
+ */
+ unfavouriteStatus(id: string): Promise<Response<Entity.Status>>;
+ /**
+ * POST /api/v1/statuses/:id/reblog
+ *
+ * @param id The target status id.
+ * @return Status.
+ */
+ reblogStatus(id: string): Promise<Response<Entity.Status>>;
+ /**
+ * POST /api/v1/statuses/:id/unreblog
+ *
+ * @param id The target status id.
+ * @return Status.
+ */
+ unreblogStatus(id: string): Promise<Response<Entity.Status>>;
+ /**
+ * POST /api/v1/statuses/:id/bookmark
+ *
+ * @param id The target status id.
+ * @return Status.
+ */
+ bookmarkStatus(id: string): Promise<Response<Entity.Status>>;
+ /**
+ * POST /api/v1/statuses/:id/unbookmark
+ *
+ * @param id The target status id.
+ * @return Status.
+ */
+ unbookmarkStatus(id: string): Promise<Response<Entity.Status>>;
+ /**
+ * POST /api/v1/statuses/:id/mute
+ *
+ * @param id The target status id.
+ * @return Status
+ */
+ muteStatus(id: string): Promise<Response<Entity.Status>>;
+ /**
+ * POST /api/v1/statuses/:id/unmute
+ *
+ * @param id The target status id.
+ * @return Status
+ */
+ unmuteStatus(id: string): Promise<Response<Entity.Status>>;
+ /**
+ * POST /api/v1/statuses/:id/pin
+ * @param id The target status id.
+ * @return Status
+ */
+ pinStatus(id: string): Promise<Response<Entity.Status>>;
+ /**
+ * POST /api/v1/statuses/:id/unpin
+ *
+ * @param id The target status id.
+ * @return Status
+ */
+ unpinStatus(id: string): Promise<Response<Entity.Status>>;
+ /**
+ * POST /api/v1/statuses/:id/react/:name
+ * @param id The target status id.
+ * @param name The name of the emoji reaction to add.
+ * @return Status
+ */
+ reactStatus(id: string, name: string): Promise<Response<Entity.Status>>;
+ /**
+ * POST /api/v1/statuses/:id/unreact/:name
+ *
+ * @param id The target status id.
+ * @param name The name of the emoji reaction to remove.
+ * @return Status
+ */
+ unreactStatus(id: string, name: string): Promise<Response<Entity.Status>>;
+ // ======================================
+ // statuses/media
+ // ======================================
+ /**
+ * POST /api/v2/media
+ *
+ * @param file The file to be attached, using multipart form data.
+ * @param options.description A plain-text description of the media.
+ * @param options.focus Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0.
+ * @return Attachment
+ */
+ uploadMedia(
+ file: any,
+ options?: { description?: string; focus?: string },
+ ): Promise<Response<Entity.Attachment | Entity.AsyncAttachment>>;
+ /**
+ * GET /api/v1/media/:id
+ *
+ * @param id Target media ID.
+ * @return Attachment
+ */
+ getMedia(id: string): Promise<Response<Entity.Attachment>>;
+ /**
+ * PUT /api/v1/media/:id
+ *
+ * @param id Target media ID.
+ * @param options.file The file to be attached, using multipart form data.
+ * @param options.description A plain-text description of the media.
+ * @param options.focus Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0.
+ * @param options.is_sensitive Whether the media is sensitive.
+ * @return Attachment
+ */
+ updateMedia(
+ id: string,
+ options?: {
+ file?: any;
+ description?: string;
+ focus?: string;
+ is_sensitive?: boolean;
+ },
+ ): Promise<Response<Entity.Attachment>>;
+ // ======================================
+ // statuses/polls
+ // ======================================
+ /**
+ * GET /api/v1/polls/:id
+ *
+ * @param id Target poll ID.
+ * @return Poll
+ */
+ getPoll(id: string): Promise<Response<Entity.Poll>>;
+ /**
+ * POST /api/v1/polls/:id/votes
+ *
+ * @param id Target poll ID.
+ * @param choices Array of own votes containing index for each option (starting from 0).
+ * @return Poll
+ */
+ votePoll(id: string, choices: Array<number>): Promise<Response<Entity.Poll>>;
+ // ======================================
+ // statuses/scheduled_statuses
+ // ======================================
+ /**
+ * GET /api/v1/scheduled_statuses
+ *
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of scheduled statuses.
+ */
+ getScheduledStatuses(options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.ScheduledStatus>>>;
+ /**
+ * GET /api/v1/scheduled_statuses/:id
+ *
+ * @param id Target status ID.
+ * @return ScheduledStatus.
+ */
+ getScheduledStatus(id: string): Promise<Response<Entity.ScheduledStatus>>;
+ /**
+ * PUT /api/v1/scheduled_statuses/:id
+ *
+ * @param id Target scheduled status ID.
+ * @param scheduled_at ISO 8601 Datetime at which the status will be published.
+ * @return ScheduledStatus.
+ */
+ scheduleStatus(
+ id: string,
+ scheduled_at?: string | null,
+ ): Promise<Response<Entity.ScheduledStatus>>;
+ /**
+ * DELETE /api/v1/scheduled_statuses/:id
+ *
+ * @param id Target scheduled status ID.
+ */
+ cancelScheduledStatus(id: string): Promise<Response<{}>>;
+ // ======================================
+ // timelines
+ // ======================================
+ /**
+ * GET /api/v1/timelines/public
+ *
+ * @param options.only_media Show only statuses with media attached? Defaults to false.
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ getPublicTimeline(options?: {
+ only_media?: boolean;
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Status>>>;
+ /**
+ * GET /api/v1/timelines/public
+ *
+ * @param options.only_media Show only statuses with media attached? Defaults to false.
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ getLocalTimeline(options?: {
+ only_media?: boolean;
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Status>>>;
+ /**
+ * GET /api/v1/timelines/tag/:hashtag
+ *
+ * @param hashtag Content of a #hashtag, not including # symbol.
+ * @param options.local Show only local statuses? Defaults to false.
+ * @param options.only_media Show only statuses with media attached? Defaults to false.
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ getTagTimeline(
+ hashtag: string,
+ options?: {
+ local?: boolean;
+ only_media?: boolean;
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ },
+ ): Promise<Response<Array<Entity.Status>>>;
+ /**
+ * GET /api/v1/timelines/home
+ *
+ * @param options.local Show only local statuses? Defaults to false.
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ getHomeTimeline(options?: {
+ local?: boolean;
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Status>>>;
+ /**
+ * GET /api/v1/timelines/list/:list_id
+ *
+ * @param list_id Local ID of the list in the database.
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ getListTimeline(
+ list_id: string,
+ options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ },
+ ): Promise<Response<Array<Entity.Status>>>;
+ // ======================================
+ // timelines/conversations
+ // ======================================
+ /**
+ * GET /api/v1/conversations
+ *
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ getConversationTimeline(options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Conversation>>>;
+ /**
+ * DELETE /api/v1/conversations/:id
+ *
+ * @param id Target conversation ID.
+ */
+ deleteConversation(id: string): Promise<Response<{}>>;
+ /**
+ * POST /api/v1/conversations/:id/read
+ *
+ * @param id Target conversation ID.
+ * @return Conversation.
+ */
+ readConversation(id: string): Promise<Response<Entity.Conversation>>;
+ // ======================================
+ // timelines/lists
+ // ======================================
+ /**
+ * GET /api/v1/lists
+ *
+ * @return Array of lists.
+ */
+ getLists(): Promise<Response<Array<Entity.List>>>;
+ /**
+ * GET /api/v1/lists/:id
+ *
+ * @param id Target list ID.
+ * @return List.
+ */
+ getList(id: string): Promise<Response<Entity.List>>;
+ /**
+ * POST /api/v1/lists
+ *
+ * @param title List name.
+ * @return List.
+ */
+ createList(title: string): Promise<Response<Entity.List>>;
+ /**
+ * PUT /api/v1/lists/:id
+ *
+ * @param id Target list ID.
+ * @param title New list name.
+ * @return List.
+ */
+ updateList(id: string, title: string): Promise<Response<Entity.List>>;
+ /**
+ * DELETE /api/v1/lists/:id
+ *
+ * @param id Target list ID.
+ */
+ deleteList(id: string): Promise<Response<{}>>;
+ /**
+ * GET /api/v1/lists/:id/accounts
+ *
+ * @param id Target list ID.
+ * @param options.limit Max number of results to return.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of accounts.
+ */
+ getAccountsInList(
+ id: string,
+ options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ },
+ ): Promise<Response<Array<Entity.Account>>>;
+ /**
+ * POST /api/v1/lists/:id/accounts
+ *
+ * @param id Target list ID.
+ * @param account_ids Array of account IDs to add to the list.
+ */
+ addAccountsToList(
+ id: string,
+ account_ids: Array<string>,
+ ): Promise<Response<{}>>;
+ /**
+ * DELETE /api/v1/lists/:id/accounts
+ *
+ * @param id Target list ID.
+ * @param account_ids Array of account IDs to add to the list.
+ */
+ deleteAccountsFromList(
+ id: string,
+ account_ids: Array<string>,
+ ): Promise<Response<{}>>;
+ // ======================================
+ // timelines/markers
+ // ======================================
+ /**
+ * GET /api/v1/markers
+ *
+ * @param timelines Array of timeline names, String enum anyOf home, notifications.
+ * @return Marker or empty object.
+ */
+ getMarkers(timeline: Array<string>): Promise<Response<Entity.Marker | {}>>;
+ /**
+ * POST /api/v1/markers
+ *
+ * @param options.home Marker position of the last read status ID in home timeline.
+ * @param options.notifications Marker position of the last read notification ID in notifications.
+ * @return Marker.
+ */
+ saveMarkers(options?: {
+ home?: { last_read_id: string };
+ notifications?: { last_read_id: string };
+ }): Promise<Response<Entity.Marker>>;
+ // ======================================
+ // notifications
+ // ======================================
+ /**
+ * GET /api/v1/notifications
+ *
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @param options.exclude_types Array of types to exclude.
+ * @param options.account_id Return only notifications received from this account.
+ * @return Array of notifications.
+ */
+ getNotifications(options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ exclude_types?: Array<Entity.NotificationType>;
+ account_id?: string;
+ }): Promise<Response<Array<Entity.Notification>>>;
+ /**
+ * GET /api/v1/notifications/:id
+ *
+ * @param id Target notification ID.
+ * @return Notification.
+ */
+ getNotification(id: string): Promise<Response<Entity.Notification>>;
+ /**
+ * POST /api/v1/notifications/clear
+ */
+ dismissNotifications(): Promise<Response<{}>>;
+ /**
+ * POST /api/v1/notifications/:id/dismiss
+ *
+ * @param id Target notification ID.
+ */
+ dismissNotification(id: string): Promise<Response<{}>>;
+ /**
+ * POST /api/v1/pleroma/notifcations/read
+ *
+ * @param id A single notification ID to read
+ * @param max_id Read all notifications up to this ID
+ * @return Array of notifications
+ */
+ readNotifications(options: { id?: string; max_id?: string }): Promise<
+ Response<Entity.Notification | Array<Entity.Notification>>
+ >;
+ // ======================================
+ // notifications/push
+ // ======================================
+ /**
+ * POST /api/v1/push/subscription
+ *
+ * @param subscription[endpoint] Endpoint URL that is called when a notification event occurs.
+ * @param subscription[keys][p256dh] User agent public key. Base64 encoded string of public key of ECDH key using prime256v1 curve.
+ * @param subscription[keys] Auth secret. Base64 encoded string of 16 bytes of random data.
+ * @param data[alerts][follow] Receive follow notifications?
+ * @param data[alerts][favourite] Receive favourite notifications?
+ * @param data[alerts][reblog] Receive reblog notifictaions?
+ * @param data[alerts][mention] Receive mention notifications?
+ * @param data[alerts][poll] Receive poll notifications?
+ * @return PushSubscription.
+ */
+ subscribePushNotification(
+ subscription: { endpoint: string; keys: { p256dh: string; auth: string } },
+ data?: {
+ alerts: {
+ follow?: boolean;
+ favourite?: boolean;
+ reblog?: boolean;
+ mention?: boolean;
+ poll?: boolean;
+ };
+ } | null,
+ ): Promise<Response<Entity.PushSubscription>>;
+ /**
+ * GET /api/v1/push/subscription
+ *
+ * @return PushSubscription.
+ */
+ getPushSubscription(): Promise<Response<Entity.PushSubscription>>;
+ /**
+ * PUT /api/v1/push/subscription
+ *
+ * @param data[alerts][follow] Receive follow notifications?
+ * @param data[alerts][favourite] Receive favourite notifications?
+ * @param data[alerts][reblog] Receive reblog notifictaions?
+ * @param data[alerts][mention] Receive mention notifications?
+ * @param data[alerts][poll] Receive poll notifications?
+ * @return PushSubscription.
+ */
+ updatePushSubscription(
+ data?: {
+ alerts: {
+ follow?: boolean;
+ favourite?: boolean;
+ reblog?: boolean;
+ mention?: boolean;
+ poll?: boolean;
+ };
+ } | null,
+ ): Promise<Response<Entity.PushSubscription>>;
+ /**
+ * DELETE /api/v1/push/subscription
+ */
+ deletePushSubscription(): Promise<Response<{}>>;
+ // ======================================
+ // search
+ // ======================================
+ /**
+ * GET /api/v2/search
+ *
+ * @param q The search query.
+ * @param type Enum of search target.
+ * @param options.limit Maximum number of results to load, per type. Defaults to 20. Max 40.
+ * @param options.max_id Return results older than this id.
+ * @param options.min_id Return results immediately newer than this id.
+ * @param options.resolve Attempt WebFinger lookup. Defaults to false.
+ * @param options.following Only include accounts that the user is following. Defaults to false.
+ * @param options.account_id If provided, statuses returned will be authored only by this account.
+ * @param options.exclude_unreviewed Filter out unreviewed tags? Defaults to false.
+ * @return Results.
+ */
+ search(
+ q: string,
+ type: "accounts" | "hashtags" | "statuses",
+ options?: {
+ limit?: number;
+ max_id?: string;
+ min_id?: string;
+ resolve?: boolean;
+ offset?: number;
+ following?: boolean;
+ account_id?: string;
+ exclude_unreviewed?: boolean;
+ },
+ ): Promise<Response<Entity.Results>>;
+
+ // ======================================
+ // instance
+ // ======================================
+ /**
+ * GET /api/v1/instance
+ */
+ getInstance(): Promise<Response<Entity.Instance>>;
+
+ /**
+ * GET /api/v1/instance/peers
+ */
+ getInstancePeers(): Promise<Response<Array<string>>>;
+
+ /**
+ * GET /api/v1/instance/activity
+ */
+ getInstanceActivity(): Promise<Response<Array<Entity.Activity>>>;
+
+ // ======================================
+ // instance/trends
+ // ======================================
+ /**
+ * GET /api/v1/trends
+ *
+ * @param limit Maximum number of results to return. Defaults to 10.
+ */
+ getInstanceTrends(
+ limit?: number | null,
+ ): Promise<Response<Array<Entity.Tag>>>;
+
+ // ======================================
+ // instance/directory
+ // ======================================
+ /**
+ * GET /api/v1/directory
+ *
+ * @param options.limit How many accounts to load. Default 40.
+ * @param options.offset How many accounts to skip before returning results. Default 0.
+ * @param options.order Order of results.
+ * @param options.local Only return local accounts.
+ * @return Array of accounts.
+ */
+ getInstanceDirectory(options?: {
+ limit?: number;
+ offset?: number;
+ order?: "active" | "new";
+ local?: boolean;
+ }): Promise<Response<Array<Entity.Account>>>;
+
+ // ======================================
+ // instance/custom_emojis
+ // ======================================
+ /**
+ * GET /api/v1/custom_emojis
+ *
+ * @return Array of emojis.
+ */
+ getInstanceCustomEmojis(): Promise<Response<Array<Entity.Emoji>>>;
+
+ // ======================================
+ // instance/announcements
+ // ======================================
+ /**
+ * GET /api/v1/announcements
+ *
+ * @param with_dismissed Include announcements dismissed by the user. Defaults to false.
+ * @return Array of announcements.
+ */
+ getInstanceAnnouncements(
+ with_dismissed?: boolean | null,
+ ): Promise<Response<Array<Entity.Announcement>>>;
+
+ /**
+ * POST /api/v1/announcements/:id/dismiss
+ */
+ dismissInstanceAnnouncement(id: string): Promise<Response<{}>>;
+
+ // ======================================
+ // Emoji reactions
+ // ======================================
+ createEmojiReaction(
+ id: string,
+ emoji: string,
+ ): Promise<Response<Entity.Status>>;
+ deleteEmojiReaction(
+ id: string,
+ emoji: string,
+ ): Promise<Response<Entity.Status>>;
+ getEmojiReactions(id: string): Promise<Response<Array<Entity.Reaction>>>;
+ getEmojiReaction(
+ id: string,
+ emoji: string,
+ ): Promise<Response<Entity.Reaction>>;
+
+ // ======================================
+ // WebSocket
+ // ======================================
+ userSocket(): WebSocketInterface;
+ publicSocket(): WebSocketInterface;
+ localSocket(): WebSocketInterface;
+ tagSocket(tag: string): WebSocketInterface;
+ listSocket(list_id: string): WebSocketInterface;
+ directSocket(): WebSocketInterface;
+}
+
+export class NoImplementedError extends Error {
+ constructor(err?: string) {
+ super(err);
+
+ this.name = new.target.name;
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
+
+export class ArgumentError extends Error {
+ constructor(err?: string) {
+ super(err);
+
+ this.name = new.target.name;
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
+
+export class UnexpectedError extends Error {
+ constructor(err?: string) {
+ super(err);
+
+ this.name = new.target.name;
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
+
+type Instance = {
+ title: string;
+ uri: string;
+ urls: {
+ streaming_api: string;
+ };
+ version: string;
+};
+
+/**
+ * Detect SNS type.
+ * Now support Mastodon, Pleroma and Pixelfed.
+ *
+ * @param url Base URL of SNS.
+ * @param proxyConfig Proxy setting, or set false if don't use proxy.
+ * @return SNS name.
+ */
+export const detector = async (
+ url: string,
+ proxyConfig: ProxyConfig | false = false,
+): Promise<"mastodon" | "pleroma" | "misskey"> => {
+ let options: AxiosRequestConfig = {
+ headers: {
+ "User-Agent": DEFAULT_UA,
+ },
+ };
+ if (proxyConfig) {
+ options = Object.assign(options, {
+ httpsAgent: proxyAgent(proxyConfig),
+ });
+ }
+ try {
+ const res = await axios.get<Instance>(url + "/api/v1/instance", options);
+ if (res.data.version.includes("Pleroma")) {
+ return "pleroma";
+ } else {
+ return "mastodon";
+ }
+ } catch (err) {
+ await axios.post<{}>(url + "/api/meta", {}, options);
+ return "misskey";
+ }
+};
+
+/**
+ * Get client for each SNS according to megalodon interface.
+ *
+ * @param baseUrl hostname or base URL.
+ * @param accessToken access token from OAuth2 authorization
+ * @param userAgent UserAgent is specified in header on request.
+ * @param proxyConfig Proxy setting, or set false if don't use proxy.
+ * @return Client instance for each SNS you specified.
+ */
+const generator = (
+ baseUrl: string,
+ accessToken: string | null = null,
+ userAgent: string | null = null,
+ proxyConfig: ProxyConfig | false = false,
+): MegalodonInterface =>
+ new Misskey(baseUrl, accessToken, userAgent, proxyConfig);
+
+export default generator;
diff --git a/packages/megalodon/src/misskey.ts b/packages/megalodon/src/misskey.ts
new file mode 100644
index 0000000000..edfaa4f3cb
--- /dev/null
+++ b/packages/megalodon/src/misskey.ts
@@ -0,0 +1,3436 @@
+import FormData from "form-data";
+import AsyncLock from "async-lock";
+
+import MisskeyAPI from "./misskey/api_client";
+import { DEFAULT_UA } from "./default";
+import { ProxyConfig } from "./proxy_config";
+import OAuth from "./oauth";
+import Response from "./response";
+import Entity from "./entity";
+import {
+ MegalodonInterface,
+ WebSocketInterface,
+ NoImplementedError,
+ ArgumentError,
+ UnexpectedError,
+} from "./megalodon";
+import MegalodonEntity from "@/entity";
+import fs from "node:fs";
+import MisskeyNotificationType from "./misskey/notification";
+
+type AccountCache = {
+ locks: AsyncLock;
+ accounts: Entity.Account[];
+};
+
+export default class Misskey implements MegalodonInterface {
+ public client: MisskeyAPI.Interface;
+ public converter: MisskeyAPI.Converter;
+ public baseUrl: string;
+ public proxyConfig: ProxyConfig | false;
+
+ /**
+ * @param baseUrl hostname or base URL
+ * @param accessToken access token from OAuth2 authorization
+ * @param userAgent UserAgent is specified in header on request.
+ * @param proxyConfig Proxy setting, or set false if don't use proxy.
+ */
+ constructor(
+ baseUrl: string,
+ accessToken: string | null = null,
+ userAgent: string | null = DEFAULT_UA,
+ proxyConfig: ProxyConfig | false = false,
+ ) {
+ let token = "";
+ if (accessToken) {
+ token = accessToken;
+ }
+ let agent: string = DEFAULT_UA;
+ if (userAgent) {
+ agent = userAgent;
+ }
+ this.converter = new MisskeyAPI.Converter(baseUrl);
+ this.client = new MisskeyAPI.Client(
+ baseUrl,
+ token,
+ agent,
+ proxyConfig,
+ this.converter,
+ );
+ this.baseUrl = baseUrl;
+ this.proxyConfig = proxyConfig;
+ }
+
+ private baseUrlToHost(baseUrl: string): string {
+ return baseUrl.replace("https://", "");
+ }
+
+ public cancel(): void {
+ return this.client.cancel();
+ }
+
+ public async registerApp(
+ client_name: string,
+ options: Partial<{
+ scopes: Array<string>;
+ redirect_uris: string;
+ website: string;
+ }> = {
+ scopes: MisskeyAPI.DEFAULT_SCOPE,
+ redirect_uris: this.baseUrl,
+ },
+ ): Promise<OAuth.AppData> {
+ return this.createApp(client_name, options).then(async (appData) => {
+ return this.generateAuthUrlAndToken(appData.client_secret).then(
+ (session) => {
+ appData.url = session.url;
+ appData.session_token = session.token;
+ return appData;
+ },
+ );
+ });
+ }
+
+ /**
+ * POST /api/app/create
+ *
+ * Create an application.
+ * @param client_name Your application's name.
+ * @param options Form data.
+ */
+ public async createApp(
+ client_name: string,
+ options: Partial<{
+ scopes: Array<string>;
+ redirect_uris: string;
+ website: string;
+ }> = {
+ scopes: MisskeyAPI.DEFAULT_SCOPE,
+ redirect_uris: this.baseUrl,
+ },
+ ): Promise<OAuth.AppData> {
+ const redirect_uris = options.redirect_uris || this.baseUrl;
+ const scopes = options.scopes || MisskeyAPI.DEFAULT_SCOPE;
+
+ const params: {
+ name: string;
+ description: string;
+ permission: Array<string>;
+ callbackUrl: string;
+ } = {
+ name: client_name,
+ description: "",
+ permission: scopes,
+ callbackUrl: redirect_uris,
+ };
+
+ /**
+ * The response is:
+ {
+ "id": "xxxxxxxxxx",
+ "name": "string",
+ "callbackUrl": "string",
+ "permission": [
+ "string"
+ ],
+ "secret": "string"
+ }
+ */
+ return this.client
+ .post<MisskeyAPI.Entity.App>("/api/app/create", params)
+ .then((res: Response<MisskeyAPI.Entity.App>) => {
+ const appData: OAuth.AppDataFromServer = {
+ id: res.data.id,
+ name: res.data.name,
+ website: null,
+ redirect_uri: res.data.callbackUrl,
+ client_id: "",
+ client_secret: res.data.secret,
+ };
+ return OAuth.AppData.from(appData);
+ });
+ }
+
+ /**
+ * POST /api/auth/session/generate
+ */
+ public async generateAuthUrlAndToken(
+ clientSecret: string,
+ ): Promise<MisskeyAPI.Entity.Session> {
+ return this.client
+ .post<MisskeyAPI.Entity.Session>("/api/auth/session/generate", {
+ appSecret: clientSecret,
+ })
+ .then((res: Response<MisskeyAPI.Entity.Session>) => res.data);
+ }
+
+ // ======================================
+ // apps
+ // ======================================
+ public async verifyAppCredentials(): Promise<Response<Entity.Application>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // apps/oauth
+ // ======================================
+ /**
+ * POST /api/auth/session/userkey
+ *
+ * @param _client_id This parameter is not used in this method.
+ * @param client_secret Application secret key which will be provided in createApp.
+ * @param session_token Session token string which will be provided in generateAuthUrlAndToken.
+ * @param _redirect_uri This parameter is not used in this method.
+ */
+ public async fetchAccessToken(
+ _client_id: string | null,
+ client_secret: string,
+ session_token: string,
+ _redirect_uri?: string,
+ ): Promise<OAuth.TokenData> {
+ return this.client
+ .post<MisskeyAPI.Entity.UserKey>("/api/auth/session/userkey", {
+ appSecret: client_secret,
+ token: session_token,
+ })
+ .then((res) => {
+ const token = new OAuth.TokenData(
+ res.data.accessToken,
+ "misskey",
+ "",
+ 0,
+ null,
+ null,
+ );
+ return token;
+ });
+ }
+
+ public async refreshToken(
+ _client_id: string,
+ _client_secret: string,
+ _refresh_token: string,
+ ): Promise<OAuth.TokenData> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async revokeToken(
+ _client_id: string,
+ _client_secret: string,
+ _token: string,
+ ): Promise<Response<{}>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // accounts
+ // ======================================
+ public async registerAccount(
+ _username: string,
+ _email: string,
+ _password: string,
+ _agreement: boolean,
+ _locale: string,
+ _reason?: string | null,
+ ): Promise<Response<Entity.Token>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ /**
+ * POST /api/i
+ */
+ public async verifyAccountCredentials(): Promise<Response<Entity.Account>> {
+ return this.client
+ .post<MisskeyAPI.Entity.UserDetail>("/api/i")
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.userDetail(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ ),
+ });
+ });
+ }
+
+ /**
+ * POST /api/i/update
+ */
+ public async updateCredentials(options?: {
+ discoverable?: boolean;
+ bot?: boolean;
+ display_name?: string;
+ note?: string;
+ avatar?: string;
+ header?: string;
+ locked?: boolean;
+ source?: {
+ privacy?: string;
+ sensitive?: boolean;
+ language?: string;
+ } | null;
+ fields_attributes?: Array<{ name: string; value: string }>;
+ }): Promise<Response<Entity.Account>> {
+ let params = {};
+ if (options) {
+ if (options.bot !== undefined) {
+ params = Object.assign(params, {
+ isBot: options.bot,
+ });
+ }
+ if (options.display_name) {
+ params = Object.assign(params, {
+ name: options.display_name,
+ });
+ }
+ if (options.note) {
+ params = Object.assign(params, {
+ description: options.note,
+ });
+ }
+ if (options.locked !== undefined) {
+ params = Object.assign(params, {
+ isLocked: options.locked,
+ });
+ }
+ if (options.source) {
+ if (options.source.language) {
+ params = Object.assign(params, {
+ lang: options.source.language,
+ });
+ }
+ if (options.source.sensitive) {
+ params = Object.assign(params, {
+ alwaysMarkNsfw: options.source.sensitive,
+ });
+ }
+ }
+ }
+ return this.client
+ .post<MisskeyAPI.Entity.UserDetail>("/api/i", params)
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.userDetail(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ ),
+ });
+ });
+ }
+
+ /**
+ * POST /api/users/show
+ */
+ public async getAccount(id: string): Promise<Response<Entity.Account>> {
+ return this.client
+ .post<MisskeyAPI.Entity.UserDetail>("/api/users/show", {
+ userId: id,
+ })
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.userDetail(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ ),
+ });
+ });
+ }
+
+ public async getAccountByName(
+ user: string,
+ host: string | null,
+ ): Promise<Response<Entity.Account>> {
+ return this.client
+ .post<MisskeyAPI.Entity.UserDetail>("/api/users/show", {
+ username: user,
+ host: host ?? null,
+ })
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.userDetail(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ ),
+ });
+ });
+ }
+
+ /**
+ * POST /api/users/notes
+ */
+ public async getAccountStatuses(
+ id: string,
+ options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ pinned?: boolean;
+ exclude_replies: boolean;
+ exclude_reblogs: boolean;
+ only_media?: boolean;
+ },
+ ): Promise<Response<Array<Entity.Status>>> {
+ const accountCache = this.getFreshAccountCache();
+
+ if (options?.pinned) {
+ return this.client
+ .post<MisskeyAPI.Entity.UserDetail>("/api/users/show", {
+ userId: id,
+ })
+ .then(async (res) => {
+ if (res.data.pinnedNotes) {
+ return {
+ ...res,
+ data: await Promise.all(
+ res.data.pinnedNotes.map((n) =>
+ this.noteWithDetails(
+ n,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ ),
+ ),
+ ),
+ };
+ }
+ return { ...res, data: [] };
+ });
+ }
+
+ let params = {
+ userId: id,
+ };
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ sinceId: options.since_id,
+ });
+ }
+ if (options.exclude_replies) {
+ params = Object.assign(params, {
+ includeReplies: false,
+ });
+ }
+ if (options.exclude_reblogs) {
+ params = Object.assign(params, {
+ includeMyRenotes: false,
+ });
+ }
+ if (options.only_media) {
+ params = Object.assign(params, {
+ withFiles: options.only_media,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Note>>("/api/users/notes", params)
+ .then(async (res) => {
+ const statuses: Array<Entity.Status> = await Promise.all(
+ res.data.map((note) =>
+ this.noteWithDetails(
+ note,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ ),
+ ),
+ );
+ return Object.assign(res, {
+ data: statuses,
+ });
+ });
+ }
+
+ public async getAccountFavourites(
+ id: string,
+ options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ },
+ ): Promise<Response<Array<Entity.Status>>> {
+ const accountCache = this.getFreshAccountCache();
+
+ let params = {
+ userId: id,
+ };
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit <= 100 ? options.limit : 100,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ sinceId: options.since_id,
+ });
+ }
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Favorite>>("/api/users/reactions", params)
+ .then(async (res) => {
+ return Object.assign(res, {
+ data: await Promise.all(
+ res.data.map((fav) =>
+ this.noteWithDetails(
+ fav.note,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ ),
+ ),
+ ),
+ });
+ });
+ }
+
+ public async subscribeAccount(
+ _id: string,
+ ): Promise<Response<Entity.Relationship>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async unsubscribeAccount(
+ _id: string,
+ ): Promise<Response<Entity.Relationship>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ /**
+ * POST /api/users/followers
+ */
+ public async getAccountFollowers(
+ id: string,
+ options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ },
+ ): Promise<Response<Array<Entity.Account>>> {
+ let params = {
+ userId: id,
+ };
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit <= 100 ? options.limit : 100,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 40,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 40,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Follower>>("/api/users/followers", params)
+ .then(async (res) => {
+ return Object.assign(res, {
+ data: await Promise.all(
+ res.data.map(async (f) =>
+ this.getAccount(f.followerId).then((p) => p.data),
+ ),
+ ),
+ });
+ });
+ }
+
+ /**
+ * POST /api/users/following
+ */
+ public async getAccountFollowing(
+ id: string,
+ options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ },
+ ): Promise<Response<Array<Entity.Account>>> {
+ let params = {
+ userId: id,
+ };
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit <= 100 ? options.limit : 100,
+ });
+ }
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Following>>("/api/users/following", params)
+ .then(async (res) => {
+ return Object.assign(res, {
+ data: await Promise.all(
+ res.data.map(async (f) =>
+ this.getAccount(f.followeeId).then((p) => p.data),
+ ),
+ ),
+ });
+ });
+ }
+
+ public async getAccountLists(
+ _id: string,
+ ): Promise<Response<Array<Entity.List>>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async getIdentityProof(
+ _id: string,
+ ): Promise<Response<Array<Entity.IdentityProof>>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ /**
+ * POST /api/following/create
+ */
+ public async followAccount(
+ id: string,
+ _options?: { reblog?: boolean },
+ ): Promise<Response<Entity.Relationship>> {
+ await this.client.post<{}>("/api/following/create", {
+ userId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
+ userId: id,
+ })
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.relation(res.data),
+ });
+ });
+ }
+
+ /**
+ * POST /api/following/delete
+ */
+ public async unfollowAccount(
+ id: string,
+ ): Promise<Response<Entity.Relationship>> {
+ await this.client.post<{}>("/api/following/delete", {
+ userId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
+ userId: id,
+ })
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.relation(res.data),
+ });
+ });
+ }
+
+ /**
+ * POST /api/blocking/create
+ */
+ public async blockAccount(
+ id: string,
+ ): Promise<Response<Entity.Relationship>> {
+ await this.client.post<{}>("/api/blocking/create", {
+ userId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
+ userId: id,
+ })
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.relation(res.data),
+ });
+ });
+ }
+
+ /**
+ * POST /api/blocking/delete
+ */
+ public async unblockAccount(
+ id: string,
+ ): Promise<Response<Entity.Relationship>> {
+ await this.client.post<{}>("/api/blocking/delete", {
+ userId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
+ userId: id,
+ })
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.relation(res.data),
+ });
+ });
+ }
+
+ /**
+ * POST /api/mute/create
+ */
+ public async muteAccount(
+ id: string,
+ _notifications: boolean,
+ ): Promise<Response<Entity.Relationship>> {
+ await this.client.post<{}>("/api/mute/create", {
+ userId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
+ userId: id,
+ })
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.relation(res.data),
+ });
+ });
+ }
+
+ /**
+ * POST /api/mute/delete
+ */
+ public async unmuteAccount(
+ id: string,
+ ): Promise<Response<Entity.Relationship>> {
+ await this.client.post<{}>("/api/mute/delete", {
+ userId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
+ userId: id,
+ })
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.relation(res.data),
+ });
+ });
+ }
+
+ public async pinAccount(_id: string): Promise<Response<Entity.Relationship>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async unpinAccount(
+ _id: string,
+ ): Promise<Response<Entity.Relationship>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ /**
+ * POST /api/users/relation
+ *
+ * @param id The accountID, for example `'1sdfag'`
+ */
+ public async getRelationship(
+ id: string,
+ ): Promise<Response<Entity.Relationship>> {
+ return this.client
+ .post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
+ userId: id,
+ })
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.relation(res.data),
+ });
+ });
+ }
+
+ /**
+ * POST /api/users/relation
+ *
+ * @param id Array of account ID, for example `['1sdfag', 'ds12aa']`.
+ */
+ public async getRelationships(
+ ids: Array<string>,
+ ): Promise<Response<Array<Entity.Relationship>>> {
+ return Promise.all(ids.map((id) => this.getRelationship(id))).then(
+ (results) => ({
+ ...results[0],
+ data: results.map((r) => r.data),
+ }),
+ );
+ }
+
+ /**
+ * POST /api/users/search
+ */
+ public async searchAccount(
+ q: string,
+ options?: {
+ following?: boolean;
+ resolve?: boolean;
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ },
+ ): Promise<Response<Array<Entity.Account>>> {
+ let params = {
+ query: q,
+ detail: true,
+ };
+ if (options) {
+ if (options.resolve !== undefined) {
+ params = Object.assign(params, {
+ localOnly: options.resolve,
+ });
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 40,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 40,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.UserDetail>>("/api/users/search", params)
+ .then((res) => {
+ return Object.assign(res, {
+ data: res.data.map((u) =>
+ this.converter.userDetail(u, this.baseUrlToHost(this.baseUrl)),
+ ),
+ });
+ });
+ }
+
+ // ======================================
+ // accounts/bookmarks
+ // ======================================
+ /**
+ * POST /api/i/favorites
+ */
+ public async getBookmarks(options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Status>>> {
+ const accountCache = this.getFreshAccountCache();
+
+ let params = {};
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit <= 100 ? options.limit : 100,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 40,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ sinceId: options.min_id,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 40,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Favorite>>("/api/i/favorites", params)
+ .then(async (res) => {
+ return Object.assign(res, {
+ data: await Promise.all(
+ res.data.map((s) =>
+ this.noteWithDetails(
+ s.note,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ ),
+ ),
+ ),
+ });
+ });
+ }
+
+ // ======================================
+ // accounts/favourites
+ // ======================================
+ public async getFavourites(options?: {
+ limit?: number;
+ max_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Status>>> {
+ const userId = await this.client
+ .post<MisskeyAPI.Entity.UserDetail>("/api/i")
+ .then((res) => res.data.id);
+ return this.getAccountFavourites(userId, options);
+ }
+
+ // ======================================
+ // accounts/mutes
+ // ======================================
+ /**
+ * POST /api/mute/list
+ */
+ public async getMutes(options?: {
+ limit?: number;
+ max_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Account>>> {
+ let params = {};
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 40,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ sinceId: options.min_id,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 40,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Mute>>("/api/mute/list", params)
+ .then((res) => {
+ return Object.assign(res, {
+ data: res.data.map((mute) =>
+ this.converter.userDetail(
+ mute.mutee,
+ this.baseUrlToHost(this.baseUrl),
+ ),
+ ),
+ });
+ });
+ }
+
+ // ======================================
+ // accounts/blocks
+ // ======================================
+ /**
+ * POST /api/blocking/list
+ */
+ public async getBlocks(options?: {
+ limit?: number;
+ max_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Account>>> {
+ let params = {};
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 40,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ sinceId: options.min_id,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 40,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Blocking>>("/api/blocking/list", params)
+ .then((res) => {
+ return Object.assign(res, {
+ data: res.data.map((blocking) =>
+ this.converter.userDetail(
+ blocking.blockee,
+ this.baseUrlToHost(this.baseUrl),
+ ),
+ ),
+ });
+ });
+ }
+
+ // ======================================
+ // accounts/domain_blocks
+ // ======================================
+ public async getDomainBlocks(_options?: {
+ limit?: number;
+ max_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<string>>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async blockDomain(_domain: string): Promise<Response<{}>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async unblockDomain(_domain: string): Promise<Response<{}>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // accounts/filters
+ // ======================================
+ public async getFilters(): Promise<Response<Array<Entity.Filter>>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async getFilter(_id: string): Promise<Response<Entity.Filter>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async createFilter(
+ _phrase: string,
+ _context: Array<string>,
+ _options?: {
+ irreversible?: boolean;
+ whole_word?: boolean;
+ expires_in?: string;
+ },
+ ): Promise<Response<Entity.Filter>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async updateFilter(
+ _id: string,
+ _phrase: string,
+ _context: Array<string>,
+ _options?: {
+ irreversible?: boolean;
+ whole_word?: boolean;
+ expires_in?: string;
+ },
+ ): Promise<Response<Entity.Filter>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async deleteFilter(_id: string): Promise<Response<Entity.Filter>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // accounts/reports
+ // ======================================
+ /**
+ * POST /api/users/report-abuse
+ */
+ public async report(
+ account_id: string,
+ comment: string,
+ _options?: {
+ status_ids?: Array<string>;
+ forward?: boolean;
+ },
+ ): Promise<Response<Entity.Report>> {
+ return this.client
+ .post<{}>("/api/users/report-abuse", {
+ userId: account_id,
+ comment: comment,
+ })
+ .then((res) => {
+ return Object.assign(res, {
+ data: {
+ id: "",
+ action_taken: "",
+ comment: comment,
+ account_id: account_id,
+ status_ids: [],
+ },
+ });
+ });
+ }
+
+ // ======================================
+ // accounts/follow_requests
+ // ======================================
+ /**
+ * POST /api/following/requests/list
+ */
+ public async getFollowRequests(
+ _limit?: number,
+ ): Promise<Response<Array<Entity.Account>>> {
+ return this.client
+ .post<Array<MisskeyAPI.Entity.FollowRequest>>(
+ "/api/following/requests/list",
+ )
+ .then((res) => {
+ return Object.assign(res, {
+ data: res.data.map((r) => this.converter.user(r.follower)),
+ });
+ });
+ }
+
+ /**
+ * POST /api/following/requests/accept
+ */
+ public async acceptFollowRequest(
+ id: string,
+ ): Promise<Response<Entity.Relationship>> {
+ await this.client.post<{}>("/api/following/requests/accept", {
+ userId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
+ userId: id,
+ })
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.relation(res.data),
+ });
+ });
+ }
+
+ /**
+ * POST /api/following/requests/reject
+ */
+ public async rejectFollowRequest(
+ id: string,
+ ): Promise<Response<Entity.Relationship>> {
+ await this.client.post<{}>("/api/following/requests/reject", {
+ userId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
+ userId: id,
+ })
+ .then((res) => {
+ return Object.assign(res, {
+ data: this.converter.relation(res.data),
+ });
+ });
+ }
+
+ // ======================================
+ // accounts/endorsements
+ // ======================================
+ public async getEndorsements(_options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ }): Promise<Response<Array<Entity.Account>>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // accounts/featured_tags
+ // ======================================
+ public async getFeaturedTags(): Promise<Response<Array<Entity.FeaturedTag>>> {
+ return this.getAccountFeaturedTags();
+ }
+
+ public async getAccountFeaturedTags(): Promise<
+ Response<Array<Entity.FeaturedTag>>
+ > {
+ const tags: Entity.FeaturedTag[] = [];
+ const res: Response = {
+ headers: undefined,
+ statusText: "",
+ status: 200,
+ data: tags,
+ };
+ return new Promise((resolve) => resolve(res));
+ }
+
+ public async createFeaturedTag(
+ _name: string,
+ ): Promise<Response<Entity.FeaturedTag>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async deleteFeaturedTag(_id: string): Promise<Response<{}>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async getSuggestedTags(): Promise<Response<Array<Entity.Tag>>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // accounts/preferences
+ // ======================================
+ public async getPreferences(): Promise<Response<Entity.Preferences>> {
+ return this.client
+ .post<MisskeyAPI.Entity.UserDetailMe>("/api/i")
+ .then(async (res) => {
+ return Object.assign(res, {
+ data: this.converter.userPreferences(
+ res.data,
+ await this.getDefaultPostPrivacy(),
+ ),
+ });
+ });
+ }
+
+ // ======================================
+ // accounts/suggestions
+ // ======================================
+ /**
+ * POST /api/users/recommendation
+ */
+ public async getSuggestions(
+ limit?: number,
+ ): Promise<Response<Array<Entity.Account>>> {
+ let params = {};
+ if (limit) {
+ params = Object.assign(params, {
+ limit: limit,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.UserDetail>>(
+ "/api/users/recommendation",
+ params,
+ )
+ .then((res) => ({
+ ...res,
+ data: res.data.map((u) =>
+ this.converter.userDetail(u, this.baseUrlToHost(this.baseUrl)),
+ ),
+ }));
+ }
+
+ // ======================================
+ // accounts/tags
+ // ======================================
+ public async getFollowedTags(): Promise<Response<Array<Entity.Tag>>> {
+ const tags: Entity.Tag[] = [];
+ const res: Response = {
+ headers: undefined,
+ statusText: "",
+ status: 200,
+ data: tags,
+ };
+ return new Promise((resolve) => resolve(res));
+ }
+
+ public async getTag(_id: string): Promise<Response<Entity.Tag>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async followTag(_id: string): Promise<Response<Entity.Tag>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async unfollowTag(_id: string): Promise<Response<Entity.Tag>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // statuses
+ // ======================================
+ public async postStatus(
+ status: string,
+ options?: {
+ media_ids?: Array<string>;
+ poll?: {
+ options: Array<string>;
+ expires_in: number;
+ multiple?: boolean;
+ hide_totals?: boolean;
+ };
+ in_reply_to_id?: string;
+ sensitive?: boolean;
+ spoiler_text?: string;
+ visibility?: "public" | "unlisted" | "private" | "direct";
+ scheduled_at?: string;
+ language?: string;
+ quote_id?: string;
+ },
+ ): Promise<Response<Entity.Status>> {
+ let params = {
+ text: status,
+ };
+ if (options) {
+ if (options.media_ids) {
+ params = Object.assign(params, {
+ fileIds: options.media_ids,
+ });
+ }
+ if (options.poll) {
+ let pollParam = {
+ choices: options.poll.options,
+ expiresAt: null,
+ expiredAfter: options.poll.expires_in * 1000,
+ };
+ if (options.poll.multiple !== undefined) {
+ pollParam = Object.assign(pollParam, {
+ multiple: options.poll.multiple,
+ });
+ }
+ params = Object.assign(params, {
+ poll: pollParam,
+ });
+ }
+ if (options.in_reply_to_id) {
+ params = Object.assign(params, {
+ replyId: options.in_reply_to_id,
+ });
+ }
+ if (options.sensitive) {
+ params = Object.assign(params, {
+ cw: "",
+ });
+ }
+ if (options.spoiler_text) {
+ params = Object.assign(params, {
+ cw: options.spoiler_text,
+ });
+ }
+ if (options.visibility) {
+ params = Object.assign(params, {
+ visibility: this.converter.encodeVisibility(options.visibility),
+ });
+ }
+ if (options.quote_id) {
+ params = Object.assign(params, {
+ renoteId: options.quote_id,
+ });
+ }
+ }
+ return this.client
+ .post<MisskeyAPI.Entity.CreatedNote>("/api/notes/create", params)
+ .then(async (res) => ({
+ ...res,
+ data: await this.noteWithDetails(
+ res.data.createdNote,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ ),
+ }));
+ }
+
+ /**
+ * POST /api/notes/show
+ */
+ public async getStatus(id: string): Promise<Response<Entity.Status>> {
+ return this.client
+ .post<MisskeyAPI.Entity.Note>("/api/notes/show", {
+ noteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: await this.noteWithDetails(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ ),
+ }));
+ }
+
+ private getFreshAccountCache(): AccountCache {
+ return {
+ locks: new AsyncLock(),
+ accounts: [],
+ };
+ }
+
+ public async notificationWithDetails(
+ n: MisskeyAPI.Entity.Notification,
+ host: string,
+ cache: AccountCache,
+ ): Promise<MegalodonEntity.Notification> {
+ const notification = this.converter.notification(n, host);
+ if (n.note)
+ notification.status = await this.noteWithDetails(n.note, host, cache);
+ if (notification.account)
+ notification.account = (
+ await this.getAccount(notification.account.id)
+ ).data;
+ return notification;
+ }
+
+ public async noteWithDetails(
+ n: MisskeyAPI.Entity.Note,
+ host: string,
+ cache: AccountCache,
+ ): Promise<MegalodonEntity.Status> {
+ const status = await this.addUserDetailsToStatus(
+ this.converter.note(n, host),
+ cache,
+ );
+ status.bookmarked = await this.isStatusBookmarked(n.id);
+ return this.addMentionsToStatus(status, cache);
+ }
+
+ public async isStatusBookmarked(id: string): Promise<boolean> {
+ return this.client
+ .post<MisskeyAPI.Entity.State>("/api/notes/state", {
+ noteId: id,
+ })
+ .then((p) => p.data.isFavorited ?? false);
+ }
+
+ public async addUserDetailsToStatus(
+ status: Entity.Status,
+ cache: AccountCache,
+ ): Promise<Entity.Status> {
+ if (
+ status.account.followers_count === 0 &&
+ status.account.followers_count === 0 &&
+ status.account.statuses_count === 0
+ )
+ status.account =
+ (await this.getAccountCached(
+ status.account.id,
+ status.account.acct,
+ cache,
+ )) ?? status.account;
+
+ if (status.reblog != null)
+ status.reblog = await this.addUserDetailsToStatus(status.reblog, cache);
+
+ if (status.quote != null)
+ status.quote = await this.addUserDetailsToStatus(status.quote, cache);
+
+ return status;
+ }
+
+ public async addMentionsToStatus(
+ status: Entity.Status,
+ cache: AccountCache,
+ ): Promise<Entity.Status> {
+ if (status.mentions.length > 0) return status;
+
+ if (status.reblog != null)
+ status.reblog = await this.addMentionsToStatus(status.reblog, cache);
+
+ if (status.quote != null)
+ status.quote = await this.addMentionsToStatus(status.quote, cache);
+
+ const idx = status.account.acct.indexOf("@");
+ const origin = idx < 0 ? null : status.account.acct.substring(idx + 1);
+
+ status.mentions = (
+ await this.getMentions(status.plain_content!, origin, cache)
+ ).filter((p) => p != null);
+ for (const m of status.mentions.filter(
+ (value, index, array) => array.indexOf(value) === index,
+ )) {
+ const regexFull = new RegExp(
+ `(?<=^|\\s|>)@${m.acct}(?=[^a-zA-Z0-9]|$)`,
+ "gi",
+ );
+ const regexLocalUser = new RegExp(
+ `(?<=^|\\s|>)@${m.acct}@${this.baseUrlToHost(
+ this.baseUrl,
+ )}(?=[^a-zA-Z0-9]|$)`,
+ "gi",
+ );
+ const regexRemoteUser = new RegExp(
+ `(?<=^|\\s|>)@${m.username}(?=[^a-zA-Z0-9@]|$)`,
+ "gi",
+ );
+
+ if (m.acct == m.username) {
+ status.content = status.content.replace(regexLocalUser, `@${m.acct}`);
+ } else if (!status.content.match(regexFull)) {
+ status.content = status.content.replace(regexRemoteUser, `@${m.acct}`);
+ }
+
+ status.content = status.content.replace(
+ regexFull,
+ `<a href="${m.url}" class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@${m.acct}</a>`,
+ );
+ }
+ return status;
+ }
+
+ public async getMentions(
+ text: string,
+ origin: string | null,
+ cache: AccountCache,
+ ): Promise<Entity.Mention[]> {
+ const mentions: Entity.Mention[] = [];
+
+ if (text == undefined) return mentions;
+
+ const mentionMatch = text.matchAll(
+ /(?<=^|\s)@(?<user>[a-zA-Z0-9_]+)(?:@(?<host>[a-zA-Z0-9-.]+\.[a-zA-Z0-9-]+)|)(?=[^a-zA-Z0-9]|$)/g,
+ );
+
+ for (const m of mentionMatch) {
+ try {
+ if (m.groups == null) continue;
+
+ const account = await this.getAccountByNameCached(
+ m.groups.user,
+ m.groups.host ?? origin,
+ cache,
+ );
+
+ if (account == null) continue;
+
+ mentions.push({
+ id: account.id,
+ url: account.url,
+ username: account.username,
+ acct: account.acct,
+ });
+ } catch {}
+ }
+
+ return mentions;
+ }
+
+ public async getAccountByNameCached(
+ user: string,
+ host: string | null,
+ cache: AccountCache,
+ ): Promise<Entity.Account | undefined | null> {
+ const acctToFind = host == null ? user : `${user}@${host}`;
+
+ return await cache.locks.acquire(acctToFind, async () => {
+ const cacheHit = cache.accounts.find((p) => p.acct === acctToFind);
+ const account =
+ cacheHit ?? (await this.getAccountByName(user, host ?? null)).data;
+
+ if (!account) {
+ return null;
+ }
+
+ if (cacheHit == null) {
+ cache.accounts.push(account);
+ }
+
+ return account;
+ });
+ }
+
+ public async getAccountCached(
+ id: string,
+ acct: string,
+ cache: AccountCache,
+ ): Promise<Entity.Account | undefined | null> {
+ return await cache.locks.acquire(acct, async () => {
+ const cacheHit = cache.accounts.find((p) => p.id === id);
+ const account = cacheHit ?? (await this.getAccount(id)).data;
+
+ if (!account) {
+ return null;
+ }
+
+ if (cacheHit == null) {
+ cache.accounts.push(account);
+ }
+
+ return account;
+ });
+ }
+
+ public async editStatus(
+ _id: string,
+ _options: {
+ status?: string;
+ spoiler_text?: string;
+ sensitive?: boolean;
+ media_ids?: Array<string>;
+ poll?: {
+ options?: Array<string>;
+ expires_in?: number;
+ multiple?: boolean;
+ hide_totals?: boolean;
+ };
+ },
+ ): Promise<Response<Entity.Status>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ /**
+ * POST /api/notes/delete
+ */
+ public async deleteStatus(id: string): Promise<Response<{}>> {
+ return this.client.post<{}>("/api/notes/delete", {
+ noteId: id,
+ });
+ }
+
+ /**
+ * POST /api/notes/children
+ */
+ public async getStatusContext(
+ id: string,
+ options?: { limit?: number; max_id?: string; since_id?: string },
+ ): Promise<Response<Entity.Context>> {
+ let params = {
+ noteId: id,
+ };
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ depth: 12,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 30,
+ depth: 12,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ sinceId: options.since_id,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 30,
+ depth: 12,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Note>>("/api/notes/children", params)
+ .then(async (res) => {
+ const accountCache = this.getFreshAccountCache();
+ const conversation = await this.client.post<
+ Array<MisskeyAPI.Entity.Note>
+ >("/api/notes/conversation", params);
+ const parents = await Promise.all(
+ conversation.data.map((n) =>
+ this.noteWithDetails(
+ n,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ ),
+ ),
+ );
+
+ const context: Entity.Context = {
+ ancestors: parents.reverse(),
+ descendants: this.dfs(
+ await Promise.all(
+ res.data.map((n) =>
+ this.noteWithDetails(
+ n,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ ),
+ ),
+ ),
+ ),
+ };
+ return {
+ ...res,
+ data: context,
+ };
+ });
+ }
+
+ private dfs(graph: Entity.Status[]) {
+ // we don't need to run dfs if we have zero or one elements
+ if (graph.length <= 1) {
+ return graph;
+ }
+
+ // sort the graph first, so we can grab the correct starting point
+ graph = graph.sort((a, b) => {
+ if (a.id < b.id) return -1;
+ if (a.id > b.id) return 1;
+ return 0;
+ });
+
+ const initialPostId = graph[0].in_reply_to_id;
+
+ // populate stack with all top level replies
+ const stack = graph
+ .filter((reply) => reply.in_reply_to_id === initialPostId)
+ .reverse();
+ const visited = new Set();
+ const result = [];
+
+ while (stack.length) {
+ const currentPost = stack.pop();
+
+ if (currentPost === undefined) return result;
+
+ if (!visited.has(currentPost)) {
+ visited.add(currentPost);
+ result.push(currentPost);
+
+ for (const reply of graph
+ .filter((reply) => reply.in_reply_to_id === currentPost.id)
+ .reverse()) {
+ stack.push(reply);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public async getStatusHistory(): Promise<Response<Array<Entity.StatusEdit>>> {
+ // FIXME: stub, implement once we have note edit history in the database
+ const history: Entity.StatusEdit[] = [];
+ const res: Response = {
+ headers: undefined,
+ statusText: "",
+ status: 200,
+ data: history,
+ };
+ return new Promise((resolve) => resolve(res));
+ }
+
+ /**
+ * POST /api/notes/renotes
+ */
+ public async getStatusRebloggedBy(
+ id: string,
+ ): Promise<Response<Array<Entity.Account>>> {
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Note>>("/api/notes/renotes", {
+ noteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: (
+ await Promise.all(res.data.map((n) => this.getAccount(n.user.id)))
+ ).map((p) => p.data),
+ }));
+ }
+
+ public async getStatusFavouritedBy(
+ id: string,
+ ): Promise<Response<Array<Entity.Account>>> {
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Reaction>>("/api/notes/reactions", {
+ noteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: (
+ await Promise.all(res.data.map((n) => this.getAccount(n.user.id)))
+ ).map((p) => p.data),
+ }));
+ }
+
+ public async favouriteStatus(id: string): Promise<Response<Entity.Status>> {
+ return this.createEmojiReaction(id, await this.getDefaultFavoriteEmoji());
+ }
+
+ private async getDefaultFavoriteEmoji(): Promise<string> {
+ // NOTE: get-unsecure is calckey's extension.
+ // Misskey doesn't have this endpoint and regular `/i/registry/get` won't work
+ // unless you have a 'nativeToken', which is reserved for the frontend webapp.
+
+ return await this.client
+ .post<Array<string>>("/api/i/registry/get-unsecure", {
+ key: "reactions",
+ scope: ["client", "base"],
+ })
+ .then((res) => res.data[0] ?? "⭐");
+ }
+
+ private async getDefaultPostPrivacy(): Promise<
+ "public" | "unlisted" | "private" | "direct"
+ > {
+ // NOTE: get-unsecure is calckey's extension.
+ // Misskey doesn't have this endpoint and regular `/i/registry/get` won't work
+ // unless you have a 'nativeToken', which is reserved for the frontend webapp.
+
+ return this.client
+ .post<string>("/api/i/registry/get-unsecure", {
+ key: "defaultNoteVisibility",
+ scope: ["client", "base"],
+ })
+ .then((res) => {
+ if (
+ !res.data ||
+ (res.data != "public" &&
+ res.data != "home" &&
+ res.data != "followers" &&
+ res.data != "specified")
+ )
+ return "public";
+ return this.converter.visibility(res.data);
+ })
+ .catch((_) => "public");
+ }
+
+ public async unfavouriteStatus(id: string): Promise<Response<Entity.Status>> {
+ // NOTE: Misskey allows only one reaction per status, so we don't need to care what that emoji was.
+ return this.deleteEmojiReaction(id, "");
+ }
+
+ /**
+ * POST /api/notes/create
+ */
+ public async reblogStatus(id: string): Promise<Response<Entity.Status>> {
+ return this.client
+ .post<MisskeyAPI.Entity.CreatedNote>("/api/notes/create", {
+ renoteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: await this.noteWithDetails(
+ res.data.createdNote,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ ),
+ }));
+ }
+
+ /**
+ * POST /api/notes/unrenote
+ */
+ public async unreblogStatus(id: string): Promise<Response<Entity.Status>> {
+ await this.client.post<{}>("/api/notes/unrenote", {
+ noteId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Note>("/api/notes/show", {
+ noteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: await this.noteWithDetails(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ ),
+ }));
+ }
+
+ /**
+ * POST /api/notes/favorites/create
+ */
+ public async bookmarkStatus(id: string): Promise<Response<Entity.Status>> {
+ await this.client.post<{}>("/api/notes/favorites/create", {
+ noteId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Note>("/api/notes/show", {
+ noteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: await this.noteWithDetails(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ ),
+ }));
+ }
+
+ /**
+ * POST /api/notes/favorites/delete
+ */
+ public async unbookmarkStatus(id: string): Promise<Response<Entity.Status>> {
+ await this.client.post<{}>("/api/notes/favorites/delete", {
+ noteId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Note>("/api/notes/show", {
+ noteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: await this.noteWithDetails(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ ),
+ }));
+ }
+
+ public async muteStatus(_id: string): Promise<Response<Entity.Status>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async unmuteStatus(_id: string): Promise<Response<Entity.Status>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ /**
+ * POST /api/i/pin
+ */
+ public async pinStatus(id: string): Promise<Response<Entity.Status>> {
+ await this.client.post<{}>("/api/i/pin", {
+ noteId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Note>("/api/notes/show", {
+ noteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: await this.noteWithDetails(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ ),
+ }));
+ }
+
+ /**
+ * POST /api/i/unpin
+ */
+ public async unpinStatus(id: string): Promise<Response<Entity.Status>> {
+ await this.client.post<{}>("/api/i/unpin", {
+ noteId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Note>("/api/notes/show", {
+ noteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: await this.noteWithDetails(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ ),
+ }));
+ }
+
+ /**
+ * Convert a Unicode emoji or custom emoji name to a Misskey reaction.
+ * @see Misskey's reaction-lib.ts
+ */
+ private reactionName(name: string): string {
+ // See: https://github.com/tc39/proposal-regexp-unicode-property-escapes#matching-emoji
+ const isUnicodeEmoji =
+ /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu.test(
+ name,
+ );
+ if (isUnicodeEmoji) {
+ return name;
+ }
+ return `:${name}:`;
+ }
+
+ /**
+ * POST /api/notes/reactions/create
+ */
+ public async reactStatus(
+ id: string,
+ name: string,
+ ): Promise<Response<Entity.Status>> {
+ await this.client.post<{}>("/api/notes/reactions/create", {
+ noteId: id,
+ reaction: this.reactionName(name),
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Note>("/api/notes/show", {
+ noteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: await this.noteWithDetails(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ ),
+ }));
+ }
+
+ /**
+ * POST /api/notes/reactions/delete
+ */
+ public async unreactStatus(
+ id: string,
+ name: string,
+ ): Promise<Response<Entity.Status>> {
+ await this.client.post<{}>("/api/notes/reactions/delete", {
+ noteId: id,
+ reaction: this.reactionName(name),
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Note>("/api/notes/show", {
+ noteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: await this.noteWithDetails(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ ),
+ }));
+ }
+
+ // ======================================
+ // statuses/media
+ // ======================================
+ /**
+ * POST /api/drive/files/create
+ */
+ public async uploadMedia(
+ file: any,
+ options?: { description?: string; focus?: string },
+ ): Promise<Response<Entity.Attachment>> {
+ const formData = new FormData();
+ formData.append("file", fs.createReadStream(file.path), {
+ contentType: file.mimetype,
+ });
+
+ if (file.originalname != null && file.originalname !== "file")
+ formData.append("name", file.originalname);
+
+ if (options?.description != null)
+ formData.append("comment", options.description);
+
+ let headers: { [key: string]: string } = {};
+ if (typeof formData.getHeaders === "function") {
+ headers = formData.getHeaders();
+ }
+ return this.client
+ .post<MisskeyAPI.Entity.File>(
+ "/api/drive/files/create",
+ formData,
+ headers,
+ )
+ .then((res) => ({ ...res, data: this.converter.file(res.data) }));
+ }
+
+ public async getMedia(id: string): Promise<Response<Entity.Attachment>> {
+ const res = await this.client.post<MisskeyAPI.Entity.File>(
+ "/api/drive/files/show",
+ { fileId: id },
+ );
+ return { ...res, data: this.converter.file(res.data) };
+ }
+
+ /**
+ * POST /api/drive/files/update
+ */
+ public async updateMedia(
+ id: string,
+ options?: {
+ file?: any;
+ description?: string;
+ focus?: string;
+ is_sensitive?: boolean;
+ },
+ ): Promise<Response<Entity.Attachment>> {
+ let params = {
+ fileId: id,
+ };
+ if (options) {
+ if (options.is_sensitive !== undefined) {
+ params = Object.assign(params, {
+ isSensitive: options.is_sensitive,
+ });
+ }
+
+ if (options.description !== undefined) {
+ params = Object.assign(params, {
+ comment: options.description,
+ });
+ }
+ }
+ return this.client
+ .post<MisskeyAPI.Entity.File>("/api/drive/files/update", params)
+ .then((res) => ({ ...res, data: this.converter.file(res.data) }));
+ }
+
+ // ======================================
+ // statuses/polls
+ // ======================================
+ public async getPoll(id: string): Promise<Response<Entity.Poll>> {
+ const res = await this.getStatus(id);
+ if (res.data.poll == null) throw new Error("poll not found");
+ return { ...res, data: res.data.poll };
+ }
+
+ /**
+ * POST /api/notes/polls/vote
+ */
+ public async votePoll(
+ id: string,
+ choices: Array<number>,
+ ): Promise<Response<Entity.Poll>> {
+ if (!id) {
+ return new Promise((_, reject) => {
+ const err = new ArgumentError("id is required");
+ reject(err);
+ });
+ }
+
+ for (const c of choices) {
+ const params = {
+ noteId: id,
+ choice: +c,
+ };
+ await this.client.post<{}>("/api/notes/polls/vote", params);
+ }
+
+ const res = await this.client
+ .post<MisskeyAPI.Entity.Note>("/api/notes/show", {
+ noteId: id,
+ })
+ .then(async (res) => {
+ const note = await this.noteWithDetails(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ );
+ return { ...res, data: note.poll };
+ });
+ if (!res.data) {
+ return new Promise((_, reject) => {
+ const err = new UnexpectedError("poll does not exist");
+ reject(err);
+ });
+ }
+ return { ...res, data: res.data };
+ }
+
+ // ======================================
+ // statuses/scheduled_statuses
+ // ======================================
+ public async getScheduledStatuses(_options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.ScheduledStatus>>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async getScheduledStatus(
+ _id: string,
+ ): Promise<Response<Entity.ScheduledStatus>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async scheduleStatus(
+ _id: string,
+ _scheduled_at?: string | null,
+ ): Promise<Response<Entity.ScheduledStatus>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async cancelScheduledStatus(_id: string): Promise<Response<{}>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // timelines
+ // ======================================
+ /**
+ * POST /api/notes/global-timeline
+ */
+ public async getPublicTimeline(options?: {
+ only_media?: boolean;
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Status>>> {
+ const accountCache = this.getFreshAccountCache();
+
+ let params = {};
+ if (options) {
+ if (options.only_media !== undefined) {
+ params = Object.assign(params, {
+ withFiles: options.only_media,
+ });
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ sinceId: options.since_id,
+ });
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ sinceId: options.min_id,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Note>>("/api/notes/global-timeline", params)
+ .then(async (res) => ({
+ ...res,
+ data: (
+ await Promise.all(
+ res.data.map((n) =>
+ this.noteWithDetails(
+ n,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ ),
+ ),
+ )
+ ).sort(this.sortByIdDesc),
+ }));
+ }
+
+ /**
+ * POST /api/notes/local-timeline
+ */
+ public async getLocalTimeline(options?: {
+ only_media?: boolean;
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Status>>> {
+ const accountCache = this.getFreshAccountCache();
+
+ let params = {};
+ if (options) {
+ if (options.only_media !== undefined) {
+ params = Object.assign(params, {
+ withFiles: options.only_media,
+ });
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ sinceId: options.since_id,
+ });
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ sinceId: options.min_id,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Note>>("/api/notes/local-timeline", params)
+ .then(async (res) => ({
+ ...res,
+ data: (
+ await Promise.all(
+ res.data.map((n) =>
+ this.noteWithDetails(
+ n,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ ),
+ ),
+ )
+ ).sort(this.sortByIdDesc),
+ }));
+ }
+
+ /**
+ * POST /api/notes/search-by-tag
+ */
+ public async getTagTimeline(
+ hashtag: string,
+ options?: {
+ local?: boolean;
+ only_media?: boolean;
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ },
+ ): Promise<Response<Array<Entity.Status>>> {
+ const accountCache = this.getFreshAccountCache();
+
+ let params = {
+ tag: hashtag,
+ };
+ if (options) {
+ if (options.only_media !== undefined) {
+ params = Object.assign(params, {
+ withFiles: options.only_media,
+ });
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ sinceId: options.since_id,
+ });
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ sinceId: options.min_id,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Note>>("/api/notes/search-by-tag", params)
+ .then(async (res) => ({
+ ...res,
+ data: (
+ await Promise.all(
+ res.data.map((n) =>
+ this.noteWithDetails(
+ n,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ ),
+ ),
+ )
+ ).sort(this.sortByIdDesc),
+ }));
+ }
+
+ /**
+ * POST /api/notes/timeline
+ */
+ public async getHomeTimeline(options?: {
+ local?: boolean;
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Status>>> {
+ const accountCache = this.getFreshAccountCache();
+
+ let params = {
+ withFiles: false,
+ };
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ sinceId: options.since_id,
+ });
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ sinceId: options.min_id,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Note>>("/api/notes/timeline", params)
+ .then(async (res) => ({
+ ...res,
+ data: (
+ await Promise.all(
+ res.data.map((n) =>
+ this.noteWithDetails(
+ n,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ ),
+ ),
+ )
+ ).sort(this.sortByIdDesc),
+ }));
+ }
+
+ /**
+ * POST /api/notes/user-list-timeline
+ */
+ public async getListTimeline(
+ list_id: string,
+ options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ },
+ ): Promise<Response<Array<Entity.Status>>> {
+ const accountCache = this.getFreshAccountCache();
+
+ let params = {
+ listId: list_id,
+ withFiles: false,
+ };
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ sinceId: options.since_id,
+ });
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ sinceId: options.min_id,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Note>>(
+ "/api/notes/user-list-timeline",
+ params,
+ )
+ .then(async (res) => ({
+ ...res,
+ data: (
+ await Promise.all(
+ res.data.map((n) =>
+ this.noteWithDetails(
+ n,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ ),
+ ),
+ )
+ ).sort(this.sortByIdDesc),
+ }));
+ }
+
+ // ======================================
+ // timelines/conversations
+ // ======================================
+ /**
+ * POST /api/notes/mentions
+ */
+ public async getConversationTimeline(options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ }): Promise<Response<Array<Entity.Conversation>>> {
+ let params = {
+ visibility: "specified",
+ };
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ sinceId: options.since_id,
+ });
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ sinceId: options.min_id,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Note>>("/api/notes/mentions", params)
+ .then((res) => ({
+ ...res,
+ data: res.data.map((n) =>
+ this.converter.noteToConversation(
+ n,
+ this.baseUrlToHost(this.baseUrl),
+ ),
+ ),
+ }));
+ // FIXME: ^ this should also parse mentions
+ }
+
+ public async deleteConversation(_id: string): Promise<Response<{}>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async readConversation(
+ _id: string,
+ ): Promise<Response<Entity.Conversation>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ private sortByIdDesc(a: Entity.Status, b: Entity.Status): number {
+ if (a.id < b.id) return 1;
+ if (a.id > b.id) return -1;
+
+ return 0;
+ }
+
+ // ======================================
+ // timelines/lists
+ // ======================================
+ /**
+ * POST /api/users/lists/list
+ */
+ public async getLists(): Promise<Response<Array<Entity.List>>> {
+ return this.client
+ .post<Array<MisskeyAPI.Entity.List>>("/api/users/lists/list")
+ .then((res) => ({
+ ...res,
+ data: res.data.map((l) => this.converter.list(l)),
+ }));
+ }
+
+ /**
+ * POST /api/users/lists/show
+ */
+ public async getList(id: string): Promise<Response<Entity.List>> {
+ return this.client
+ .post<MisskeyAPI.Entity.List>("/api/users/lists/show", {
+ listId: id,
+ })
+ .then((res) => ({ ...res, data: this.converter.list(res.data) }));
+ }
+
+ /**
+ * POST /api/users/lists/create
+ */
+ public async createList(title: string): Promise<Response<Entity.List>> {
+ return this.client
+ .post<MisskeyAPI.Entity.List>("/api/users/lists/create", {
+ name: title,
+ })
+ .then((res) => ({ ...res, data: this.converter.list(res.data) }));
+ }
+
+ /**
+ * POST /api/users/lists/update
+ */
+ public async updateList(
+ id: string,
+ title: string,
+ ): Promise<Response<Entity.List>> {
+ return this.client
+ .post<MisskeyAPI.Entity.List>("/api/users/lists/update", {
+ listId: id,
+ name: title,
+ })
+ .then((res) => ({ ...res, data: this.converter.list(res.data) }));
+ }
+
+ /**
+ * POST /api/users/lists/delete
+ */
+ public async deleteList(id: string): Promise<Response<{}>> {
+ return this.client.post<{}>("/api/users/lists/delete", {
+ listId: id,
+ });
+ }
+
+ /**
+ * POST /api/users/lists/show
+ */
+ public async getAccountsInList(
+ id: string,
+ _options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ },
+ ): Promise<Response<Array<Entity.Account>>> {
+ const res = await this.client.post<MisskeyAPI.Entity.List>(
+ "/api/users/lists/show",
+ {
+ listId: id,
+ },
+ );
+ const promise = res.data.userIds.map((userId) => this.getAccount(userId));
+ const accounts = await Promise.all(promise);
+ return { ...res, data: accounts.map((r) => r.data) };
+ }
+
+ /**
+ * POST /api/users/lists/push
+ */
+ public async addAccountsToList(
+ id: string,
+ account_ids: Array<string>,
+ ): Promise<Response<{}>> {
+ return this.client.post<{}>("/api/users/lists/push", {
+ listId: id,
+ userId: account_ids[0],
+ });
+ }
+
+ /**
+ * POST /api/users/lists/pull
+ */
+ public async deleteAccountsFromList(
+ id: string,
+ account_ids: Array<string>,
+ ): Promise<Response<{}>> {
+ return this.client.post<{}>("/api/users/lists/pull", {
+ listId: id,
+ userId: account_ids[0],
+ });
+ }
+
+ // ======================================
+ // timelines/markers
+ // ======================================
+ public async getMarkers(
+ _timeline: Array<string>,
+ ): Promise<Response<Entity.Marker | {}>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async saveMarkers(_options?: {
+ home?: { last_read_id: string };
+ notifications?: { last_read_id: string };
+ }): Promise<Response<Entity.Marker>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // notifications
+ // ======================================
+ /**
+ * POST /api/i/notifications
+ */
+ public async getNotifications(options?: {
+ limit?: number;
+ max_id?: string;
+ since_id?: string;
+ min_id?: string;
+ exclude_type?: Array<Entity.NotificationType>;
+ account_id?: string;
+ }): Promise<Response<Array<Entity.Notification>>> {
+ let params = {};
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit <= 100 ? options.limit : 100,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ sinceId: options.since_id,
+ });
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ sinceId: options.min_id,
+ });
+ }
+ if (options.exclude_type) {
+ params = Object.assign(params, {
+ excludeType: options.exclude_type.map((e) =>
+ this.converter.encodeNotificationType(e),
+ ),
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ const cache = this.getFreshAccountCache();
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Notification>>(
+ "/api/i/notifications",
+ params,
+ )
+ .then(async (res) => ({
+ ...res,
+ data: await Promise.all(
+ res.data
+ .filter(
+ (p) => p.type != MisskeyNotificationType.FollowRequestAccepted,
+ ) // these aren't supported on mastodon
+ .map((n) =>
+ this.notificationWithDetails(
+ n,
+ this.baseUrlToHost(this.baseUrl),
+ cache,
+ ),
+ ),
+ ),
+ }));
+ }
+
+ public async getNotification(
+ _id: string,
+ ): Promise<Response<Entity.Notification>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ /**
+ * POST /api/notifications/mark-all-as-read
+ */
+ public async dismissNotifications(): Promise<Response<{}>> {
+ return this.client.post<{}>("/api/notifications/mark-all-as-read");
+ }
+
+ public async dismissNotification(_id: string): Promise<Response<{}>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async readNotifications(_options: {
+ id?: string;
+ max_id?: string;
+ }): Promise<Response<Entity.Notification | Array<Entity.Notification>>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("mastodon does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // notifications/push
+ // ======================================
+ public async subscribePushNotification(
+ _subscription: { endpoint: string; keys: { p256dh: string; auth: string } },
+ _data?: {
+ alerts: {
+ follow?: boolean;
+ favourite?: boolean;
+ reblog?: boolean;
+ mention?: boolean;
+ poll?: boolean;
+ };
+ } | null,
+ ): Promise<Response<Entity.PushSubscription>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async getPushSubscription(): Promise<
+ Response<Entity.PushSubscription>
+ > {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async updatePushSubscription(
+ _data?: {
+ alerts: {
+ follow?: boolean;
+ favourite?: boolean;
+ reblog?: boolean;
+ mention?: boolean;
+ poll?: boolean;
+ };
+ } | null,
+ ): Promise<Response<Entity.PushSubscription>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ /**
+ * DELETE /api/v1/push/subscription
+ */
+ public async deletePushSubscription(): Promise<Response<{}>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // search
+ // ======================================
+ public async search(
+ q: string,
+ type: "accounts" | "hashtags" | "statuses",
+ options?: {
+ limit?: number;
+ max_id?: string;
+ min_id?: string;
+ resolve?: boolean;
+ offset?: number;
+ following?: boolean;
+ account_id?: string;
+ exclude_unreviewed?: boolean;
+ },
+ ): Promise<Response<Entity.Results>> {
+ const accountCache = this.getFreshAccountCache();
+
+ switch (type) {
+ case "accounts": {
+ if (q.startsWith("http://") || q.startsWith("https://")) {
+ return this.client
+ .post("/api/ap/show", { uri: q })
+ .then(async (res) => {
+ if (res.status != 200 || res.data.type != "User") {
+ res.status = 200;
+ res.statusText = "OK";
+ res.data = {
+ accounts: [],
+ statuses: [],
+ hashtags: [],
+ };
+
+ return res;
+ }
+
+ const account = await this.converter.userDetail(
+ res.data.object as MisskeyAPI.Entity.UserDetail,
+ this.baseUrlToHost(this.baseUrl),
+ );
+
+ return {
+ ...res,
+ data: {
+ accounts:
+ options?.max_id && options?.max_id >= account.id
+ ? []
+ : [account],
+ statuses: [],
+ hashtags: [],
+ },
+ };
+ });
+ }
+ let params = {
+ query: q,
+ };
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+ if (options.offset) {
+ params = Object.assign(params, {
+ offset: options.offset,
+ });
+ }
+ if (options.resolve) {
+ params = Object.assign(params, {
+ localOnly: options.resolve,
+ });
+ }
+ } else {
+ params = Object.assign(params, {
+ limit: 20,
+ });
+ }
+
+ try {
+ const match = q.match(
+ /^@(?<user>[a-zA-Z0-9_]+)(?:@(?<host>[a-zA-Z0-9-.]+\.[a-zA-Z0-9-]+)|)$/,
+ );
+ if (match) {
+ const lookupQuery = {
+ username: match.groups?.user,
+ host: match.groups?.host,
+ };
+
+ const result = await this.client
+ .post<MisskeyAPI.Entity.UserDetail>(
+ "/api/users/show",
+ lookupQuery,
+ )
+ .then((res) => ({
+ ...res,
+ data: {
+ accounts: [
+ this.converter.userDetail(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ ),
+ ],
+ statuses: [],
+ hashtags: [],
+ },
+ }));
+
+ if (result.status !== 200) {
+ result.status = 200;
+ result.statusText = "OK";
+ result.data = {
+ accounts: [],
+ statuses: [],
+ hashtags: [],
+ };
+ }
+
+ return result;
+ }
+ } catch {}
+
+ return this.client
+ .post<Array<MisskeyAPI.Entity.UserDetail>>(
+ "/api/users/search",
+ params,
+ )
+ .then((res) => ({
+ ...res,
+ data: {
+ accounts: res.data.map((u) =>
+ this.converter.userDetail(u, this.baseUrlToHost(this.baseUrl)),
+ ),
+ statuses: [],
+ hashtags: [],
+ },
+ }));
+ }
+ case "statuses": {
+ if (q.startsWith("http://") || q.startsWith("https://")) {
+ return this.client
+ .post("/api/ap/show", { uri: q })
+ .then(async (res) => {
+ if (res.status != 200 || res.data.type != "Note") {
+ res.status = 200;
+ res.statusText = "OK";
+ res.data = {
+ accounts: [],
+ statuses: [],
+ hashtags: [],
+ };
+
+ return res;
+ }
+
+ const post = await this.noteWithDetails(
+ res.data.object as MisskeyAPI.Entity.Note,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ );
+
+ return {
+ ...res,
+ data: {
+ accounts: [],
+ statuses:
+ options?.max_id && options.max_id >= post.id ? [] : [post],
+ hashtags: [],
+ },
+ };
+ });
+ }
+ let params = {
+ query: q,
+ };
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ }
+ if (options.offset) {
+ params = Object.assign(params, {
+ offset: options.offset,
+ });
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ untilId: options.max_id,
+ });
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ sinceId: options.min_id,
+ });
+ }
+ if (options.account_id) {
+ params = Object.assign(params, {
+ userId: options.account_id,
+ });
+ }
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Note>>("/api/notes/search", params)
+ .then(async (res) => ({
+ ...res,
+ data: {
+ accounts: [],
+ statuses: await Promise.all(
+ res.data.map((n) =>
+ this.noteWithDetails(
+ n,
+ this.baseUrlToHost(this.baseUrl),
+ accountCache,
+ ),
+ ),
+ ),
+ hashtags: [],
+ },
+ }));
+ }
+ case "hashtags": {
+ let params = {
+ query: q,
+ };
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit,
+ });
+ }
+ if (options.offset) {
+ params = Object.assign(params, {
+ offset: options.offset,
+ });
+ }
+ }
+ return this.client
+ .post<Array<string>>("/api/hashtags/search", params)
+ .then((res) => ({
+ ...res,
+ data: {
+ accounts: [],
+ statuses: [],
+ hashtags: res.data.map((h) => ({
+ name: h,
+ url: h,
+ history: null,
+ following: false,
+ })),
+ },
+ }));
+ }
+ }
+ }
+
+ // ======================================
+ // instance
+ // ======================================
+ /**
+ * POST /api/meta
+ * POST /api/stats
+ */
+ public async getInstance(): Promise<Response<Entity.Instance>> {
+ const meta = await this.client
+ .post<MisskeyAPI.Entity.Meta>("/api/meta", { "detail": true })
+ .then((res) => res.data);
+ return this.client
+ .post<MisskeyAPI.Entity.Stats>("/api/stats", { "detail": true })
+ .then((res) => ({ ...res, data: this.converter.meta(meta, res.data) }));
+ }
+
+ public async getInstancePeers(): Promise<Response<Array<string>>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public async getInstanceActivity(): Promise<
+ Response<Array<Entity.Activity>>
+ > {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // instance/trends
+ // ======================================
+ /**
+ * POST /api/hashtags/trend
+ */
+ public async getInstanceTrends(
+ _limit?: number | null,
+ ): Promise<Response<Array<Entity.Tag>>> {
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Hashtag>>("/api/hashtags/trend")
+ .then((res) => ({
+ ...res,
+ data: res.data.map((h) => this.converter.hashtag(h)),
+ }));
+ }
+
+ // ======================================
+ // instance/directory
+ // ======================================
+ public async getInstanceDirectory(_options?: {
+ limit?: number;
+ offset?: number;
+ order?: "active" | "new";
+ local?: boolean;
+ }): Promise<Response<Array<Entity.Account>>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ // ======================================
+ // instance/custom_emojis
+ // ======================================
+ /**
+ * POST /api/meta
+ */
+ public async getInstanceCustomEmojis(): Promise<
+ Response<Array<Entity.Emoji>>
+ > {
+ return this.client
+ .post<any>("/api/emojis")
+ .then((res) => ({
+ ...res,
+ data: res.data.emojis.map((e: any) => this.converter.emoji(e)),
+ }));
+ }
+
+ // ======================================
+ // instance/announcements
+ // ======================================
+ public async getInstanceAnnouncements(
+ with_dismissed?: boolean | null,
+ ): Promise<Response<Array<Entity.Announcement>>> {
+ let params = {};
+ if (with_dismissed) {
+ params = Object.assign(params, {
+ withUnreads: with_dismissed,
+ });
+ }
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Announcement>>("/api/announcements", params)
+ .then((res) => ({
+ ...res,
+ data: res.data.map((t) => this.converter.announcement(t)),
+ }));
+ }
+
+ public async dismissInstanceAnnouncement(id: string): Promise<Response<{}>> {
+ return this.client.post<{}>("/api/i/read-announcement", {
+ announcementId: id,
+ });
+ }
+
+ // ======================================
+ // Emoji reactions
+ // ======================================
+ /**
+ * POST /api/notes/reactions/create
+ *
+ * @param {string} id Target note ID.
+ * @param {string} emoji Reaction emoji string. This string is raw unicode emoji.
+ */
+ public async createEmojiReaction(
+ id: string,
+ emoji: string,
+ ): Promise<Response<Entity.Status>> {
+ await this.client.post<{}>("/api/notes/reactions/create", {
+ noteId: id,
+ reaction: emoji,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Note>("/api/notes/show", {
+ noteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: await this.noteWithDetails(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ ),
+ }));
+ }
+
+ /**
+ * POST /api/notes/reactions/delete
+ */
+ public async deleteEmojiReaction(
+ id: string,
+ _emoji: string,
+ ): Promise<Response<Entity.Status>> {
+ await this.client.post<{}>("/api/notes/reactions/delete", {
+ noteId: id,
+ });
+ return this.client
+ .post<MisskeyAPI.Entity.Note>("/api/notes/show", {
+ noteId: id,
+ })
+ .then(async (res) => ({
+ ...res,
+ data: await this.noteWithDetails(
+ res.data,
+ this.baseUrlToHost(this.baseUrl),
+ this.getFreshAccountCache(),
+ ),
+ }));
+ }
+
+ public async getEmojiReactions(
+ id: string,
+ ): Promise<Response<Array<Entity.Reaction>>> {
+ return this.client
+ .post<Array<MisskeyAPI.Entity.Reaction>>("/api/notes/reactions", {
+ noteId: id,
+ })
+ .then((res) => ({
+ ...res,
+ data: this.converter.reactions(res.data),
+ }));
+ }
+
+ public async getEmojiReaction(
+ _id: string,
+ _emoji: string,
+ ): Promise<Response<Entity.Reaction>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError("misskey does not support");
+ reject(err);
+ });
+ }
+
+ public userSocket(): WebSocketInterface {
+ return this.client.socket("user");
+ }
+
+ public publicSocket(): WebSocketInterface {
+ return this.client.socket("globalTimeline");
+ }
+
+ public localSocket(): WebSocketInterface {
+ return this.client.socket("localTimeline");
+ }
+
+ public tagSocket(_tag: string): WebSocketInterface {
+ throw new NoImplementedError("TODO: implement");
+ }
+
+ public listSocket(list_id: string): WebSocketInterface {
+ return this.client.socket("list", list_id);
+ }
+
+ public directSocket(): WebSocketInterface {
+ return this.client.socket("conversation");
+ }
+}
diff --git a/packages/megalodon/src/misskey/api_client.ts b/packages/megalodon/src/misskey/api_client.ts
new file mode 100644
index 0000000000..a0b01030d8
--- /dev/null
+++ b/packages/megalodon/src/misskey/api_client.ts
@@ -0,0 +1,727 @@
+import axios, { AxiosResponse, AxiosRequestConfig } from "axios";
+import dayjs from "dayjs";
+import FormData from "form-data";
+
+import { DEFAULT_UA } from "../default";
+import proxyAgent, { ProxyConfig } from "../proxy_config";
+import Response from "../response";
+import MisskeyEntity from "./entity";
+import MegalodonEntity from "../entity";
+import WebSocket from "./web_socket";
+import MisskeyNotificationType from "./notification";
+import NotificationType from "../notification";
+
+namespace MisskeyAPI {
+ export namespace Entity {
+ export type App = MisskeyEntity.App;
+ export type Announcement = MisskeyEntity.Announcement;
+ export type Blocking = MisskeyEntity.Blocking;
+ export type Choice = MisskeyEntity.Choice;
+ export type CreatedNote = MisskeyEntity.CreatedNote;
+ export type Emoji = MisskeyEntity.Emoji;
+ export type Favorite = MisskeyEntity.Favorite;
+ export type Field = MisskeyEntity.Field;
+ export type File = MisskeyEntity.File;
+ export type Follower = MisskeyEntity.Follower;
+ export type Following = MisskeyEntity.Following;
+ export type FollowRequest = MisskeyEntity.FollowRequest;
+ export type Hashtag = MisskeyEntity.Hashtag;
+ export type List = MisskeyEntity.List;
+ export type Meta = MisskeyEntity.Meta;
+ export type Mute = MisskeyEntity.Mute;
+ export type Note = MisskeyEntity.Note;
+ export type Notification = MisskeyEntity.Notification;
+ export type Poll = MisskeyEntity.Poll;
+ export type Reaction = MisskeyEntity.Reaction;
+ export type Relation = MisskeyEntity.Relation;
+ export type User = MisskeyEntity.User;
+ export type UserDetail = MisskeyEntity.UserDetail;
+ export type UserDetailMe = MisskeyEntity.UserDetailMe;
+ export type GetAll = MisskeyEntity.GetAll;
+ export type UserKey = MisskeyEntity.UserKey;
+ export type Session = MisskeyEntity.Session;
+ export type Stats = MisskeyEntity.Stats;
+ export type State = MisskeyEntity.State;
+ export type APIEmoji = { emojis: Emoji[] };
+ }
+
+ export class Converter {
+ private baseUrl: string;
+ private instanceHost: string;
+ private plcUrl: string;
+ private modelOfAcct = {
+ id: "1",
+ username: "none",
+ acct: "none",
+ display_name: "none",
+ locked: true,
+ bot: true,
+ discoverable: false,
+ group: false,
+ created_at: "1971-01-01T00:00:00.000Z",
+ note: "",
+ url: "plc",
+ avatar: "plc",
+ avatar_static: "plc",
+ header: "plc",
+ header_static: "plc",
+ followers_count: -1,
+ following_count: 0,
+ statuses_count: 0,
+ last_status_at: "1971-01-01T00:00:00.000Z",
+ noindex: true,
+ emojis: [],
+ fields: [],
+ moved: null,
+ };
+
+ constructor(baseUrl: string) {
+ this.baseUrl = baseUrl;
+ this.instanceHost = baseUrl.substring(baseUrl.indexOf("//") + 2);
+ this.plcUrl = `${baseUrl}/static-assets/transparent.png`;
+ this.modelOfAcct.url = this.plcUrl;
+ this.modelOfAcct.avatar = this.plcUrl;
+ this.modelOfAcct.avatar_static = this.plcUrl;
+ this.modelOfAcct.header = this.plcUrl;
+ this.modelOfAcct.header_static = this.plcUrl;
+ }
+
+ // FIXME: Properly render MFM instead of just escaping HTML characters.
+ escapeMFM = (text: string): string =>
+ text
+ .replace(/&/g, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&#39;")
+ .replace(/`/g, "&#x60;")
+ .replace(/\r?\n/g, "<br>");
+
+ emoji = (e: Entity.Emoji): MegalodonEntity.Emoji => {
+ return {
+ shortcode: e.name,
+ static_url: e.url,
+ url: e.url,
+ visible_in_picker: true,
+ category: e.category,
+ };
+ };
+
+ field = (f: Entity.Field): MegalodonEntity.Field => ({
+ name: f.name,
+ value: this.escapeMFM(f.value),
+ verified_at: null,
+ });
+
+ user = (u: Entity.User): MegalodonEntity.Account => {
+ let acct = u.username;
+ let acctUrl = `https://${u.host || this.instanceHost}/@${u.username}`;
+ if (u.host) {
+ acct = `${u.username}@${u.host}`;
+ acctUrl = `https://${u.host}/@${u.username}`;
+ }
+ return {
+ id: u.id,
+ username: u.username,
+ acct: acct,
+ display_name: u.name || u.username,
+ locked: false,
+ created_at: new Date().toISOString(),
+ followers_count: 0,
+ following_count: 0,
+ statuses_count: 0,
+ note: "",
+ url: acctUrl,
+ avatar: u.avatarUrl,
+ avatar_static: u.avatarUrl,
+ header: this.plcUrl,
+ header_static: this.plcUrl,
+ emojis: u.emojis.map((e) => this.emoji(e)),
+ moved: null,
+ fields: [],
+ bot: false,
+ };
+ };
+
+ userDetail = (
+ u: Entity.UserDetail,
+ host: string,
+ ): MegalodonEntity.Account => {
+ let acct = u.username;
+ host = host.replace("https://", "");
+ let acctUrl = `https://${host || u.host || this.instanceHost}/@${
+ u.username
+ }`;
+ if (u.host) {
+ acct = `${u.username}@${u.host}`;
+ acctUrl = `https://${u.host}/@${u.username}`;
+ }
+ return {
+ id: u.id,
+ username: u.username,
+ acct: acct,
+ display_name: u.name || u.username,
+ locked: u.isLocked,
+ created_at: u.createdAt,
+ followers_count: u.followersCount,
+ following_count: u.followingCount,
+ statuses_count: u.notesCount,
+ note: u.description?.replace(/\n|\\n/g, "<br>") ?? "",
+ url: acctUrl,
+ avatar: u.avatarUrl,
+ avatar_static: u.avatarUrl,
+ header: u.bannerUrl ?? this.plcUrl,
+ header_static: u.bannerUrl ?? this.plcUrl,
+ emojis: u.emojis && u.emojis.length > 0 ? u.emojis.map((e) => this.emoji(e)) : [],
+ moved: null,
+ fields: u.fields.map((f) => this.field(f)),
+ bot: u.isBot,
+ };
+ };
+
+ userPreferences = (
+ u: MisskeyAPI.Entity.UserDetailMe,
+ v: "public" | "unlisted" | "private" | "direct",
+ ): MegalodonEntity.Preferences => {
+ return {
+ "reading:expand:media": "default",
+ "reading:expand:spoilers": false,
+ "posting:default:language": u.lang,
+ "posting:default:sensitive": u.alwaysMarkNsfw,
+ "posting:default:visibility": v,
+ };
+ };
+
+ visibility = (
+ v: "public" | "home" | "followers" | "specified",
+ ): "public" | "unlisted" | "private" | "direct" => {
+ switch (v) {
+ case "public":
+ return v;
+ case "home":
+ return "unlisted";
+ case "followers":
+ return "private";
+ case "specified":
+ return "direct";
+ }
+ };
+
+ encodeVisibility = (
+ v: "public" | "unlisted" | "private" | "direct",
+ ): "public" | "home" | "followers" | "specified" => {
+ switch (v) {
+ case "public":
+ return v;
+ case "unlisted":
+ return "home";
+ case "private":
+ return "followers";
+ case "direct":
+ return "specified";
+ }
+ };
+
+ fileType = (
+ s: string,
+ ): "unknown" | "image" | "gifv" | "video" | "audio" => {
+ if (s === "image/gif") {
+ return "gifv";
+ }
+ if (s.includes("image")) {
+ return "image";
+ }
+ if (s.includes("video")) {
+ return "video";
+ }
+ if (s.includes("audio")) {
+ return "audio";
+ }
+ return "unknown";
+ };
+
+ file = (f: Entity.File): MegalodonEntity.Attachment => {
+ return {
+ id: f.id,
+ type: this.fileType(f.type),
+ url: f.url,
+ remote_url: f.url,
+ preview_url: f.thumbnailUrl,
+ text_url: f.url,
+ meta: {
+ width: f.properties.width,
+ height: f.properties.height,
+ },
+ description: f.comment,
+ blurhash: f.blurhash,
+ };
+ };
+
+ follower = (f: Entity.Follower): MegalodonEntity.Account => {
+ return this.user(f.follower);
+ };
+
+ following = (f: Entity.Following): MegalodonEntity.Account => {
+ return this.user(f.followee);
+ };
+
+ relation = (r: Entity.Relation): MegalodonEntity.Relationship => {
+ return {
+ id: r.id,
+ following: r.isFollowing,
+ followed_by: r.isFollowed,
+ blocking: r.isBlocking,
+ blocked_by: r.isBlocked,
+ muting: r.isMuted,
+ muting_notifications: false,
+ requested: r.hasPendingFollowRequestFromYou,
+ domain_blocking: false,
+ showing_reblogs: true,
+ endorsed: false,
+ notifying: false,
+ };
+ };
+
+ choice = (c: Entity.Choice): MegalodonEntity.PollOption => {
+ return {
+ title: c.text,
+ votes_count: c.votes,
+ };
+ };
+
+ poll = (p: Entity.Poll, id: string): MegalodonEntity.Poll => {
+ const now = dayjs();
+ const expire = dayjs(p.expiresAt);
+ const count = p.choices.reduce((sum, choice) => sum + choice.votes, 0);
+ return {
+ id: id,
+ expires_at: p.expiresAt,
+ expired: now.isAfter(expire),
+ multiple: p.multiple,
+ votes_count: count,
+ options: p.choices.map((c) => this.choice(c)),
+ voted: p.choices.some((c) => c.isVoted),
+ own_votes: p.choices
+ .filter((c) => c.isVoted)
+ .map((c) => p.choices.indexOf(c)),
+ };
+ };
+
+ note = (n: Entity.Note, host: string): MegalodonEntity.Status => {
+ host = host.replace("https://", "");
+
+ return {
+ id: n.id,
+ uri: n.uri ? n.uri : `https://${host}/notes/${n.id}`,
+ url: n.uri ? n.uri : `https://${host}/notes/${n.id}`,
+ account: this.user(n.user),
+ in_reply_to_id: n.replyId,
+ in_reply_to_account_id: n.reply?.userId ?? null,
+ reblog: n.renote ? this.note(n.renote, host) : null,
+ content: n.text ? this.escapeMFM(n.text) : "",
+ plain_content: n.text ? n.text : null,
+ created_at: n.createdAt,
+ // Remove reaction emojis with names containing @ from the emojis list.
+ emojis: n.emojis
+ .filter((e) => e.name.indexOf("@") === -1)
+ .map((e) => this.emoji(e)),
+ replies_count: n.repliesCount,
+ reblogs_count: n.renoteCount,
+ favourites_count: this.getTotalReactions(n.reactions),
+ reblogged: false,
+ favourited: !!n.myReaction,
+ muted: false,
+ sensitive: n.files ? n.files.some((f) => f.isSensitive) : false,
+ spoiler_text: n.cw ? n.cw : "",
+ visibility: this.visibility(n.visibility),
+ media_attachments: n.files ? n.files.map((f) => this.file(f)) : [],
+ mentions: [],
+ tags: [],
+ card: null,
+ poll: n.poll ? this.poll(n.poll, n.id) : null,
+ application: null,
+ language: null,
+ pinned: null,
+ // Use emojis list to provide URLs for emoji reactions.
+ reactions: this.mapReactions(n.emojis, n.reactions, n.myReaction),
+ bookmarked: false,
+ quote: n.renote && n.text ? this.note(n.renote, host) : null,
+ };
+ };
+
+ mapReactions = (
+ emojis: Array<MisskeyEntity.Emoji>,
+ r: { [key: string]: number },
+ myReaction?: string,
+ ): Array<MegalodonEntity.Reaction> => {
+ // Map of emoji shortcodes to image URLs.
+ const emojiUrls = new Map<string, string>(
+ emojis.map((e) => [e.name, e.url]),
+ );
+ return Object.keys(r).map((key) => {
+ // Strip colons from custom emoji reaction names to match emoji shortcodes.
+ const shortcode = key.replaceAll(":", "");
+ // If this is a custom emoji (vs. a Unicode emoji), find its image URL.
+ const url = emojiUrls.get(shortcode);
+ // Finally, remove trailing @. from local custom emoji reaction names.
+ const name = shortcode.replace("@.", "");
+ return {
+ count: r[key],
+ me: key === myReaction,
+ name,
+ url,
+ // We don't actually have a static version of the asset, but clients expect one anyway.
+ static_url: url,
+ };
+ });
+ };
+
+ getTotalReactions = (r: { [key: string]: number }): number => {
+ return Object.values(r).length > 0
+ ? Object.values(r).reduce(
+ (previousValue, currentValue) => previousValue + currentValue,
+ )
+ : 0;
+ };
+
+ reactions = (
+ r: Array<Entity.Reaction>,
+ ): Array<MegalodonEntity.Reaction> => {
+ const result: Array<MegalodonEntity.Reaction> = [];
+ for (const e of r) {
+ const i = result.findIndex((res) => res.name === e.type);
+ if (i >= 0) {
+ result[i].count++;
+ } else {
+ result.push({
+ count: 1,
+ me: false,
+ name: e.type,
+ });
+ }
+ }
+ return result;
+ };
+
+ noteToConversation = (
+ n: Entity.Note,
+ host: string,
+ ): MegalodonEntity.Conversation => {
+ const accounts: Array<MegalodonEntity.Account> = [this.user(n.user)];
+ if (n.reply) {
+ accounts.push(this.user(n.reply.user));
+ }
+ return {
+ id: n.id,
+ accounts: accounts,
+ last_status: this.note(n, host),
+ unread: false,
+ };
+ };
+
+ list = (l: Entity.List): MegalodonEntity.List => ({
+ id: l.id,
+ title: l.name,
+ });
+
+ encodeNotificationType = (
+ e: MegalodonEntity.NotificationType,
+ ): MisskeyEntity.NotificationType => {
+ switch (e) {
+ case NotificationType.Follow:
+ return MisskeyNotificationType.Follow;
+ case NotificationType.Mention:
+ return MisskeyNotificationType.Reply;
+ case NotificationType.Favourite:
+ case NotificationType.Reaction:
+ return MisskeyNotificationType.Reaction;
+ case NotificationType.Reblog:
+ return MisskeyNotificationType.Renote;
+ case NotificationType.Poll:
+ return MisskeyNotificationType.PollEnded;
+ case NotificationType.FollowRequest:
+ return MisskeyNotificationType.ReceiveFollowRequest;
+ default:
+ return e;
+ }
+ };
+
+ decodeNotificationType = (
+ e: MisskeyEntity.NotificationType,
+ ): MegalodonEntity.NotificationType => {
+ switch (e) {
+ case MisskeyNotificationType.Follow:
+ return NotificationType.Follow;
+ case MisskeyNotificationType.Mention:
+ case MisskeyNotificationType.Reply:
+ return NotificationType.Mention;
+ case MisskeyNotificationType.Renote:
+ case MisskeyNotificationType.Quote:
+ return NotificationType.Reblog;
+ case MisskeyNotificationType.Reaction:
+ return NotificationType.Reaction;
+ case MisskeyNotificationType.PollEnded:
+ return NotificationType.Poll;
+ case MisskeyNotificationType.ReceiveFollowRequest:
+ return NotificationType.FollowRequest;
+ case MisskeyNotificationType.FollowRequestAccepted:
+ return NotificationType.Follow;
+ default:
+ return e;
+ }
+ };
+
+ announcement = (a: Entity.Announcement): MegalodonEntity.Announcement => ({
+ id: a.id,
+ content: `<h1>${this.escapeMFM(a.title)}</h1>${this.escapeMFM(a.text)}`,
+ starts_at: null,
+ ends_at: null,
+ published: true,
+ all_day: false,
+ published_at: a.createdAt,
+ updated_at: a.updatedAt,
+ read: a.isRead,
+ mentions: [],
+ statuses: [],
+ tags: [],
+ emojis: [],
+ reactions: [],
+ });
+
+ notification = (
+ n: Entity.Notification,
+ host: string,
+ ): MegalodonEntity.Notification => {
+ let notification = {
+ id: n.id,
+ account: n.user ? this.user(n.user) : this.modelOfAcct,
+ created_at: n.createdAt,
+ type: this.decodeNotificationType(n.type),
+ };
+ if (n.note) {
+ notification = Object.assign(notification, {
+ status: this.note(n.note, host),
+ });
+ if (notification.type === NotificationType.Poll) {
+ notification = Object.assign(notification, {
+ account: this.note(n.note, host).account,
+ });
+ }
+ if (n.reaction) {
+ notification = Object.assign(notification, {
+ reaction: this.mapReactions(n.note.emojis, { [n.reaction]: 1 })[0],
+ });
+ }
+ }
+ return notification;
+ };
+
+ stats = (s: Entity.Stats): MegalodonEntity.Stats => {
+ return {
+ user_count: s.usersCount,
+ status_count: s.notesCount,
+ domain_count: s.instances,
+ };
+ };
+
+ meta = (m: Entity.Meta, s: Entity.Stats): MegalodonEntity.Instance => {
+ const wss = m.uri.replace(/^https:\/\//, "wss://");
+ return {
+ uri: m.uri,
+ title: m.name,
+ description: m.description,
+ email: m.maintainerEmail,
+ version: m.version,
+ thumbnail: m.bannerUrl,
+ urls: {
+ streaming_api: `${wss}/streaming`,
+ },
+ stats: this.stats(s),
+ languages: m.langs,
+ contact_account: null,
+ max_toot_chars: m.maxNoteTextLength,
+ registrations: !m.disableRegistration,
+ };
+ };
+
+ hashtag = (h: Entity.Hashtag): MegalodonEntity.Tag => {
+ return {
+ name: h.tag,
+ url: h.tag,
+ history: null,
+ following: false,
+ };
+ };
+ }
+
+ export const DEFAULT_SCOPE = [
+ "read:account",
+ "write:account",
+ "read:blocks",
+ "write:blocks",
+ "read:drive",
+ "write:drive",
+ "read:favorites",
+ "write:favorites",
+ "read:following",
+ "write:following",
+ "read:mutes",
+ "write:mutes",
+ "write:notes",
+ "read:notifications",
+ "write:notifications",
+ "read:reactions",
+ "write:reactions",
+ "write:votes",
+ ];
+
+ /**
+ * Interface
+ */
+ export interface Interface {
+ post<T = any>(
+ path: string,
+ params?: any,
+ headers?: { [key: string]: string },
+ ): Promise<Response<T>>;
+ cancel(): void;
+ socket(
+ channel:
+ | "user"
+ | "localTimeline"
+ | "hybridTimeline"
+ | "globalTimeline"
+ | "conversation"
+ | "list",
+ listId?: string,
+ ): WebSocket;
+ }
+
+ /**
+ * Misskey API client.
+ *
+ * Usign axios for request, you will handle promises.
+ */
+ export class Client implements Interface {
+ private accessToken: string | null;
+ private baseUrl: string;
+ private userAgent: string;
+ private abortController: AbortController;
+ private proxyConfig: ProxyConfig | false = false;
+ private converter: Converter;
+
+ /**
+ * @param baseUrl hostname or base URL
+ * @param accessToken access token from OAuth2 authorization
+ * @param userAgent UserAgent is specified in header on request.
+ * @param proxyConfig Proxy setting, or set false if don't use proxy.
+ * @param converter Converter instance.
+ */
+ constructor(
+ baseUrl: string,
+ accessToken: string | null,
+ userAgent: string = DEFAULT_UA,
+ proxyConfig: ProxyConfig | false = false,
+ converter: Converter,
+ ) {
+ this.accessToken = accessToken;
+ this.baseUrl = baseUrl;
+ this.userAgent = userAgent;
+ this.proxyConfig = proxyConfig;
+ this.abortController = new AbortController();
+ this.converter = converter;
+ axios.defaults.signal = this.abortController.signal;
+ }
+
+ /**
+ * POST request to mastodon REST API.
+ * @param path relative path from baseUrl
+ * @param params Form data
+ * @param headers Request header object
+ */
+ public async post<T>(
+ path: string,
+ params: any = {},
+ headers: { [key: string]: string } = {},
+ ): Promise<Response<T>> {
+ let options: AxiosRequestConfig = {
+ headers: headers,
+ maxContentLength: Infinity,
+ maxBodyLength: Infinity,
+ };
+ if (this.proxyConfig) {
+ options = Object.assign(options, {
+ httpAgent: proxyAgent(this.proxyConfig),
+ httpsAgent: proxyAgent(this.proxyConfig),
+ });
+ }
+ let bodyParams = params;
+ if (this.accessToken) {
+ if (params instanceof FormData) {
+ bodyParams.append("i", this.accessToken);
+ } else {
+ bodyParams = Object.assign(params, {
+ i: this.accessToken,
+ });
+ }
+ }
+
+ return axios
+ .post<T>(this.baseUrl + path, bodyParams, options)
+ .then((resp: AxiosResponse<T>) => {
+ const res: Response<T> = {
+ data: resp.data,
+ status: resp.status,
+ statusText: resp.statusText,
+ headers: resp.headers,
+ };
+ return res;
+ });
+ }
+
+ /**
+ * Cancel all requests in this instance.
+ * @returns void
+ */
+ public cancel(): void {
+ return this.abortController.abort();
+ }
+
+ /**
+ * Get connection and receive websocket connection for Misskey API.
+ *
+ * @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list.
+ * @param listId This parameter is required only list channel.
+ */
+ public socket(
+ channel:
+ | "user"
+ | "localTimeline"
+ | "hybridTimeline"
+ | "globalTimeline"
+ | "conversation"
+ | "list",
+ listId?: string,
+ ): WebSocket {
+ if (!this.accessToken) {
+ throw new Error("accessToken is required");
+ }
+ const url = `${this.baseUrl}/streaming`;
+ const streaming = new WebSocket(
+ url,
+ channel,
+ this.accessToken,
+ listId,
+ this.userAgent,
+ this.proxyConfig,
+ this.converter,
+ );
+ process.nextTick(() => {
+ streaming.start();
+ });
+ return streaming;
+ }
+ }
+}
+
+export default MisskeyAPI;
diff --git a/packages/megalodon/src/misskey/entities/GetAll.ts b/packages/megalodon/src/misskey/entities/GetAll.ts
new file mode 100644
index 0000000000..94ace2f184
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/GetAll.ts
@@ -0,0 +1,6 @@
+namespace MisskeyEntity {
+ export type GetAll = {
+ tutorial: number;
+ defaultNoteVisibility: "public" | "home" | "followers" | "specified";
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/announcement.ts b/packages/megalodon/src/misskey/entities/announcement.ts
new file mode 100644
index 0000000000..7594ba7efc
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/announcement.ts
@@ -0,0 +1,10 @@
+namespace MisskeyEntity {
+ export type Announcement = {
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ text: string;
+ title: string;
+ isRead?: boolean;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/app.ts b/packages/megalodon/src/misskey/entities/app.ts
new file mode 100644
index 0000000000..5924060d81
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/app.ts
@@ -0,0 +1,9 @@
+namespace MisskeyEntity {
+ export type App = {
+ id: string;
+ name: string;
+ callbackUrl: string;
+ permission: Array<string>;
+ secret: string;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/blocking.ts b/packages/megalodon/src/misskey/entities/blocking.ts
new file mode 100644
index 0000000000..3e56790a7b
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/blocking.ts
@@ -0,0 +1,10 @@
+/// <reference path="userDetail.ts" />
+
+namespace MisskeyEntity {
+ export type Blocking = {
+ id: string;
+ createdAt: string;
+ blockeeId: string;
+ blockee: UserDetail;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/createdNote.ts b/packages/megalodon/src/misskey/entities/createdNote.ts
new file mode 100644
index 0000000000..235f7063fb
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/createdNote.ts
@@ -0,0 +1,7 @@
+/// <reference path="note.ts" />
+
+namespace MisskeyEntity {
+ export type CreatedNote = {
+ createdNote: Note;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/emoji.ts b/packages/megalodon/src/misskey/entities/emoji.ts
new file mode 100644
index 0000000000..d320760e91
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/emoji.ts
@@ -0,0 +1,9 @@
+namespace MisskeyEntity {
+ export type Emoji = {
+ name: string;
+ host: string | null;
+ url: string;
+ aliases: Array<string>;
+ category: string;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/favorite.ts b/packages/megalodon/src/misskey/entities/favorite.ts
new file mode 100644
index 0000000000..ba948f2e73
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/favorite.ts
@@ -0,0 +1,10 @@
+/// <reference path="note.ts" />
+
+namespace MisskeyEntity {
+ export type Favorite = {
+ id: string;
+ createdAt: string;
+ noteId: string;
+ note: Note;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/field.ts b/packages/megalodon/src/misskey/entities/field.ts
new file mode 100644
index 0000000000..8bbb2d7c42
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/field.ts
@@ -0,0 +1,7 @@
+namespace MisskeyEntity {
+ export type Field = {
+ name: string;
+ value: string;
+ verified?: string;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/file.ts b/packages/megalodon/src/misskey/entities/file.ts
new file mode 100644
index 0000000000..e823dde1be
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/file.ts
@@ -0,0 +1,20 @@
+namespace MisskeyEntity {
+ export type File = {
+ id: string;
+ createdAt: string;
+ name: string;
+ type: string;
+ md5: string;
+ size: number;
+ isSensitive: boolean;
+ properties: {
+ width: number;
+ height: number;
+ avgColor: string;
+ };
+ url: string;
+ thumbnailUrl: string;
+ comment: string;
+ blurhash: string;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/followRequest.ts b/packages/megalodon/src/misskey/entities/followRequest.ts
new file mode 100644
index 0000000000..60bd0e0abc
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/followRequest.ts
@@ -0,0 +1,9 @@
+/// <reference path="user.ts" />
+
+namespace MisskeyEntity {
+ export type FollowRequest = {
+ id: string;
+ follower: User;
+ followee: User;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/follower.ts b/packages/megalodon/src/misskey/entities/follower.ts
new file mode 100644
index 0000000000..34ae825519
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/follower.ts
@@ -0,0 +1,11 @@
+/// <reference path="userDetail.ts" />
+
+namespace MisskeyEntity {
+ export type Follower = {
+ id: string;
+ createdAt: string;
+ followeeId: string;
+ followerId: string;
+ follower: UserDetail;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/following.ts b/packages/megalodon/src/misskey/entities/following.ts
new file mode 100644
index 0000000000..6cbc8f1c39
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/following.ts
@@ -0,0 +1,11 @@
+/// <reference path="userDetail.ts" />
+
+namespace MisskeyEntity {
+ export type Following = {
+ id: string;
+ createdAt: string;
+ followeeId: string;
+ followerId: string;
+ followee: UserDetail;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/hashtag.ts b/packages/megalodon/src/misskey/entities/hashtag.ts
new file mode 100644
index 0000000000..3ec4d6675b
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/hashtag.ts
@@ -0,0 +1,7 @@
+namespace MisskeyEntity {
+ export type Hashtag = {
+ tag: string;
+ chart: Array<number>;
+ usersCount: number;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/list.ts b/packages/megalodon/src/misskey/entities/list.ts
new file mode 100644
index 0000000000..60706592a4
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/list.ts
@@ -0,0 +1,8 @@
+namespace MisskeyEntity {
+ export type List = {
+ id: string;
+ createdAt: string;
+ name: string;
+ userIds: Array<string>;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/meta.ts b/packages/megalodon/src/misskey/entities/meta.ts
new file mode 100644
index 0000000000..97827fe8fd
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/meta.ts
@@ -0,0 +1,18 @@
+/// <reference path="emoji.ts" />
+
+namespace MisskeyEntity {
+ export type Meta = {
+ maintainerName: string;
+ maintainerEmail: string;
+ name: string;
+ version: string;
+ uri: string;
+ description: string;
+ langs: Array<string>;
+ disableRegistration: boolean;
+ disableLocalTimeline: boolean;
+ bannerUrl: string;
+ maxNoteTextLength: 3000;
+ emojis: Array<Emoji>;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/mute.ts b/packages/megalodon/src/misskey/entities/mute.ts
new file mode 100644
index 0000000000..7975b3d315
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/mute.ts
@@ -0,0 +1,10 @@
+/// <reference path="userDetail.ts" />
+
+namespace MisskeyEntity {
+ export type Mute = {
+ id: string;
+ createdAt: string;
+ muteeId: string;
+ mutee: UserDetail;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/note.ts b/packages/megalodon/src/misskey/entities/note.ts
new file mode 100644
index 0000000000..64a0a50785
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/note.ts
@@ -0,0 +1,32 @@
+/// <reference path="user.ts" />
+/// <reference path="emoji.ts" />
+/// <reference path="file.ts" />
+/// <reference path="poll.ts" />
+
+namespace MisskeyEntity {
+ export type Note = {
+ id: string;
+ createdAt: string;
+ userId: string;
+ user: User;
+ text: string | null;
+ cw: string | null;
+ visibility: "public" | "home" | "followers" | "specified";
+ renoteCount: number;
+ repliesCount: number;
+ reactions: { [key: string]: number };
+ emojis: Array<Emoji>;
+ fileIds: Array<string>;
+ files: Array<File>;
+ replyId: string | null;
+ renoteId: string | null;
+ uri?: string;
+ reply?: Note;
+ renote?: Note;
+ viaMobile?: boolean;
+ tags?: Array<string>;
+ poll?: Poll;
+ mentions?: Array<string>;
+ myReaction?: string;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/notification.ts b/packages/megalodon/src/misskey/entities/notification.ts
new file mode 100644
index 0000000000..7ecb911537
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/notification.ts
@@ -0,0 +1,17 @@
+/// <reference path="user.ts" />
+/// <reference path="note.ts" />
+
+namespace MisskeyEntity {
+ export type Notification = {
+ id: string;
+ createdAt: string;
+ // https://github.com/syuilo/misskey/blob/056942391aee135eb6c77aaa63f6ed5741d701a6/src/models/entities/notification.ts#L50-L62
+ type: NotificationType;
+ userId: string;
+ user: User;
+ note?: Note;
+ reaction?: string;
+ };
+
+ export type NotificationType = string;
+}
diff --git a/packages/megalodon/src/misskey/entities/poll.ts b/packages/megalodon/src/misskey/entities/poll.ts
new file mode 100644
index 0000000000..9f6bfa40d2
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/poll.ts
@@ -0,0 +1,13 @@
+namespace MisskeyEntity {
+ export type Choice = {
+ text: string;
+ votes: number;
+ isVoted: boolean;
+ };
+
+ export type Poll = {
+ multiple: boolean;
+ expiresAt: string;
+ choices: Array<Choice>;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/reaction.ts b/packages/megalodon/src/misskey/entities/reaction.ts
new file mode 100644
index 0000000000..b35a25bfb5
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/reaction.ts
@@ -0,0 +1,11 @@
+/// <reference path="user.ts" />
+
+namespace MisskeyEntity {
+ export type Reaction = {
+ id: string;
+ createdAt: string;
+ user: User;
+ url?: string;
+ type: string;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/relation.ts b/packages/megalodon/src/misskey/entities/relation.ts
new file mode 100644
index 0000000000..6db4a1b167
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/relation.ts
@@ -0,0 +1,12 @@
+namespace MisskeyEntity {
+ export type Relation = {
+ id: string;
+ isFollowing: boolean;
+ hasPendingFollowRequestFromYou: boolean;
+ hasPendingFollowRequestToYou: boolean;
+ isFollowed: boolean;
+ isBlocking: boolean;
+ isBlocked: boolean;
+ isMuted: boolean;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/session.ts b/packages/megalodon/src/misskey/entities/session.ts
new file mode 100644
index 0000000000..572333ff0b
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/session.ts
@@ -0,0 +1,6 @@
+namespace MisskeyEntity {
+ export type Session = {
+ token: string;
+ url: string;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/state.ts b/packages/megalodon/src/misskey/entities/state.ts
new file mode 100644
index 0000000000..62d60ce282
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/state.ts
@@ -0,0 +1,7 @@
+namespace MisskeyEntity {
+ export type State = {
+ isFavorited: boolean;
+ isMutedThread: boolean;
+ isWatching: boolean;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/stats.ts b/packages/megalodon/src/misskey/entities/stats.ts
new file mode 100644
index 0000000000..9832a0ad8a
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/stats.ts
@@ -0,0 +1,9 @@
+namespace MisskeyEntity {
+ export type Stats = {
+ notesCount: number;
+ originalNotesCount: number;
+ usersCount: number;
+ originalUsersCount: number;
+ instances: number;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/user.ts b/packages/megalodon/src/misskey/entities/user.ts
new file mode 100644
index 0000000000..96610f6e6d
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/user.ts
@@ -0,0 +1,13 @@
+/// <reference path="emoji.ts" />
+
+namespace MisskeyEntity {
+ export type User = {
+ id: string;
+ name: string;
+ username: string;
+ host: string | null;
+ avatarUrl: string;
+ avatarColor: string;
+ emojis: Array<Emoji>;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/userDetail.ts b/packages/megalodon/src/misskey/entities/userDetail.ts
new file mode 100644
index 0000000000..0f5bd5f644
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/userDetail.ts
@@ -0,0 +1,34 @@
+/// <reference path="emoji.ts" />
+/// <reference path="field.ts" />
+/// <reference path="note.ts" />
+
+namespace MisskeyEntity {
+ export type UserDetail = {
+ id: string;
+ name: string;
+ username: string;
+ host: string | null;
+ avatarUrl: string;
+ avatarColor: string;
+ isAdmin: boolean;
+ isModerator: boolean;
+ isBot: boolean;
+ isCat: boolean;
+ emojis: Array<Emoji>;
+ createdAt: string;
+ bannerUrl: string;
+ bannerColor: string;
+ isLocked: boolean;
+ isSilenced: boolean;
+ isSuspended: boolean;
+ description: string;
+ followersCount: number;
+ followingCount: number;
+ notesCount: number;
+ avatarId: string;
+ bannerId: string;
+ pinnedNoteIds?: Array<string>;
+ pinnedNotes?: Array<Note>;
+ fields: Array<Field>;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/userDetailMe.ts b/packages/megalodon/src/misskey/entities/userDetailMe.ts
new file mode 100644
index 0000000000..272e65ffa4
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/userDetailMe.ts
@@ -0,0 +1,36 @@
+/// <reference path="emoji.ts" />
+/// <reference path="field.ts" />
+/// <reference path="note.ts" />
+
+namespace MisskeyEntity {
+ export type UserDetailMe = {
+ id: string;
+ name: string;
+ username: string;
+ host: string | null;
+ avatarUrl: string;
+ avatarColor: string;
+ isAdmin: boolean;
+ isModerator: boolean;
+ isBot: boolean;
+ isCat: boolean;
+ emojis: Array<Emoji>;
+ createdAt: string;
+ bannerUrl: string;
+ bannerColor: string;
+ isLocked: boolean;
+ isSilenced: boolean;
+ isSuspended: boolean;
+ description: string;
+ followersCount: number;
+ followingCount: number;
+ notesCount: number;
+ avatarId: string;
+ bannerId: string;
+ pinnedNoteIds?: Array<string>;
+ pinnedNotes?: Array<Note>;
+ fields: Array<Field>;
+ alwaysMarkNsfw: boolean;
+ lang: string | null;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entities/userkey.ts b/packages/megalodon/src/misskey/entities/userkey.ts
new file mode 100644
index 0000000000..921af65536
--- /dev/null
+++ b/packages/megalodon/src/misskey/entities/userkey.ts
@@ -0,0 +1,8 @@
+/// <reference path="user.ts" />
+
+namespace MisskeyEntity {
+ export type UserKey = {
+ accessToken: string;
+ user: User;
+ };
+}
diff --git a/packages/megalodon/src/misskey/entity.ts b/packages/megalodon/src/misskey/entity.ts
new file mode 100644
index 0000000000..72a80f9d96
--- /dev/null
+++ b/packages/megalodon/src/misskey/entity.ts
@@ -0,0 +1,28 @@
+/// <reference path="entities/app.ts" />
+/// <reference path="entities/announcement.ts" />
+/// <reference path="entities/blocking.ts" />
+/// <reference path="entities/createdNote.ts" />
+/// <reference path="entities/emoji.ts" />
+/// <reference path="entities/favorite.ts" />
+/// <reference path="entities/field.ts" />
+/// <reference path="entities/file.ts" />
+/// <reference path="entities/follower.ts" />
+/// <reference path="entities/following.ts" />
+/// <reference path="entities/followRequest.ts" />
+/// <reference path="entities/hashtag.ts" />
+/// <reference path="entities/list.ts" />
+/// <reference path="entities/meta.ts" />
+/// <reference path="entities/mute.ts" />
+/// <reference path="entities/note.ts" />
+/// <reference path="entities/notification.ts" />
+/// <reference path="entities/poll.ts" />
+/// <reference path="entities/reaction.ts" />
+/// <reference path="entities/relation.ts" />
+/// <reference path="entities/user.ts" />
+/// <reference path="entities/userDetail.ts" />
+/// <reference path="entities/userDetailMe.ts" />
+/// <reference path="entities/userkey.ts" />
+/// <reference path="entities/session.ts" />
+/// <reference path="entities/stats.ts" />
+
+export default MisskeyEntity;
diff --git a/packages/megalodon/src/misskey/notification.ts b/packages/megalodon/src/misskey/notification.ts
new file mode 100644
index 0000000000..eb7c2d23d8
--- /dev/null
+++ b/packages/megalodon/src/misskey/notification.ts
@@ -0,0 +1,18 @@
+import MisskeyEntity from "./entity";
+
+namespace MisskeyNotificationType {
+ export const Follow: MisskeyEntity.NotificationType = "follow";
+ export const Mention: MisskeyEntity.NotificationType = "mention";
+ export const Reply: MisskeyEntity.NotificationType = "reply";
+ export const Renote: MisskeyEntity.NotificationType = "renote";
+ export const Quote: MisskeyEntity.NotificationType = "quote";
+ export const Reaction: MisskeyEntity.NotificationType = "favourite";
+ export const PollEnded: MisskeyEntity.NotificationType = "pollEnded";
+ export const ReceiveFollowRequest: MisskeyEntity.NotificationType =
+ "receiveFollowRequest";
+ export const FollowRequestAccepted: MisskeyEntity.NotificationType =
+ "followRequestAccepted";
+ export const GroupInvited: MisskeyEntity.NotificationType = "groupInvited";
+}
+
+export default MisskeyNotificationType;
diff --git a/packages/megalodon/src/misskey/web_socket.ts b/packages/megalodon/src/misskey/web_socket.ts
new file mode 100644
index 0000000000..0cbfc2bfeb
--- /dev/null
+++ b/packages/megalodon/src/misskey/web_socket.ts
@@ -0,0 +1,458 @@
+import WS from "ws";
+import dayjs, { Dayjs } from "dayjs";
+import { v4 as uuid } from "uuid";
+import { EventEmitter } from "events";
+import { WebSocketInterface } from "../megalodon";
+import proxyAgent, { ProxyConfig } from "../proxy_config";
+import MisskeyAPI from "./api_client";
+
+/**
+ * WebSocket
+ * Misskey is not support http streaming. It supports websocket instead of streaming.
+ * So this class connect to Misskey server with WebSocket.
+ */
+export default class WebSocket
+ extends EventEmitter
+ implements WebSocketInterface
+{
+ public url: string;
+ public channel:
+ | "user"
+ | "localTimeline"
+ | "hybridTimeline"
+ | "globalTimeline"
+ | "conversation"
+ | "list";
+ public parser: any;
+ public headers: { [key: string]: string };
+ public proxyConfig: ProxyConfig | false = false;
+ public listId: string | null = null;
+ private _converter: MisskeyAPI.Converter;
+ private _accessToken: string;
+ private _reconnectInterval: number;
+ private _reconnectMaxAttempts: number;
+ private _reconnectCurrentAttempts: number;
+ private _connectionClosed: boolean;
+ private _client: WS | null = null;
+ private _channelID: string;
+ private _pongReceivedTimestamp: Dayjs;
+ private _heartbeatInterval = 60000;
+ private _pongWaiting = false;
+
+ /**
+ * @param url Full url of websocket: e.g. wss://misskey.io/streaming
+ * @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list.
+ * @param accessToken The access token.
+ * @param listId This parameter is required when you specify list as channel.
+ */
+ constructor(
+ url: string,
+ channel:
+ | "user"
+ | "localTimeline"
+ | "hybridTimeline"
+ | "globalTimeline"
+ | "conversation"
+ | "list",
+ accessToken: string,
+ listId: string | undefined,
+ userAgent: string,
+ proxyConfig: ProxyConfig | false = false,
+ converter: MisskeyAPI.Converter,
+ ) {
+ super();
+ this.url = url;
+ this.parser = new Parser();
+ this.channel = channel;
+ this.headers = {
+ "User-Agent": userAgent,
+ };
+ if (listId === undefined) {
+ this.listId = null;
+ } else {
+ this.listId = listId;
+ }
+ this.proxyConfig = proxyConfig;
+ this._accessToken = accessToken;
+ this._reconnectInterval = 10000;
+ this._reconnectMaxAttempts = Infinity;
+ this._reconnectCurrentAttempts = 0;
+ this._connectionClosed = false;
+ this._channelID = uuid();
+ this._pongReceivedTimestamp = dayjs();
+ this._converter = converter;
+ }
+
+ /**
+ * Start websocket connection.
+ */
+ public start() {
+ this._connectionClosed = false;
+ this._resetRetryParams();
+ this._startWebSocketConnection();
+ }
+
+ private baseUrlToHost(baseUrl: string): string {
+ return baseUrl.replace("https://", "");
+ }
+
+ /**
+ * Reset connection and start new websocket connection.
+ */
+ private _startWebSocketConnection() {
+ this._resetConnection();
+ this._setupParser();
+ this._client = this._connect();
+ this._bindSocket(this._client);
+ }
+
+ /**
+ * Stop current connection.
+ */
+ public stop() {
+ this._connectionClosed = true;
+ this._resetConnection();
+ this._resetRetryParams();
+ }
+
+ /**
+ * Clean up current connection, and listeners.
+ */
+ private _resetConnection() {
+ if (this._client) {
+ this._client.close(1000);
+ this._client.removeAllListeners();
+ this._client = null;
+ }
+
+ if (this.parser) {
+ this.parser.removeAllListeners();
+ }
+ }
+
+ /**
+ * Resets the parameters used in reconnect.
+ */
+ private _resetRetryParams() {
+ this._reconnectCurrentAttempts = 0;
+ }
+
+ /**
+ * Connect to the endpoint.
+ */
+ private _connect(): WS {
+ let options: WS.ClientOptions = {
+ headers: this.headers,
+ };
+ if (this.proxyConfig) {
+ options = Object.assign(options, {
+ agent: proxyAgent(this.proxyConfig),
+ });
+ }
+ const cli: WS = new WS(`${this.url}?i=${this._accessToken}`, options);
+ return cli;
+ }
+
+ /**
+ * Connect specified channels in websocket.
+ */
+ private _channel() {
+ if (!this._client) {
+ return;
+ }
+ switch (this.channel) {
+ case "conversation":
+ this._client.send(
+ JSON.stringify({
+ type: "connect",
+ body: {
+ channel: "main",
+ id: this._channelID,
+ },
+ }),
+ );
+ break;
+ case "user":
+ this._client.send(
+ JSON.stringify({
+ type: "connect",
+ body: {
+ channel: "main",
+ id: this._channelID,
+ },
+ }),
+ );
+ this._client.send(
+ JSON.stringify({
+ type: "connect",
+ body: {
+ channel: "homeTimeline",
+ id: this._channelID,
+ },
+ }),
+ );
+ break;
+ case "list":
+ this._client.send(
+ JSON.stringify({
+ type: "connect",
+ body: {
+ channel: "userList",
+ id: this._channelID,
+ params: {
+ listId: this.listId,
+ },
+ },
+ }),
+ );
+ break;
+ default:
+ this._client.send(
+ JSON.stringify({
+ type: "connect",
+ body: {
+ channel: this.channel,
+ id: this._channelID,
+ },
+ }),
+ );
+ break;
+ }
+ }
+
+ /**
+ * Reconnects to the same endpoint.
+ */
+
+ private _reconnect() {
+ setTimeout(() => {
+ // Skip reconnect when client is connecting.
+ // https://github.com/websockets/ws/blob/7.2.1/lib/websocket.js#L365
+ if (this._client && this._client.readyState === WS.CONNECTING) {
+ return;
+ }
+
+ if (this._reconnectCurrentAttempts < this._reconnectMaxAttempts) {
+ this._reconnectCurrentAttempts++;
+ this._clearBinding();
+ if (this._client) {
+ // In reconnect, we want to close the connection immediately,
+ // because recoonect is necessary when some problems occur.
+ this._client.terminate();
+ }
+ // Call connect methods
+ console.log("Reconnecting");
+ this._client = this._connect();
+ this._bindSocket(this._client);
+ }
+ }, this._reconnectInterval);
+ }
+
+ /**
+ * Clear binding event for websocket client.
+ */
+ private _clearBinding() {
+ if (this._client) {
+ this._client.removeAllListeners("close");
+ this._client.removeAllListeners("pong");
+ this._client.removeAllListeners("open");
+ this._client.removeAllListeners("message");
+ this._client.removeAllListeners("error");
+ }
+ }
+
+ /**
+ * Bind event for web socket client.
+ * @param client A WebSocket instance.
+ */
+ private _bindSocket(client: WS) {
+ client.on("close", (code: number, _reason: Buffer) => {
+ if (code === 1000) {
+ this.emit("close", {});
+ } else {
+ console.log(`Closed connection with ${code}`);
+ if (!this._connectionClosed) {
+ this._reconnect();
+ }
+ }
+ });
+ client.on("pong", () => {
+ this._pongWaiting = false;
+ this.emit("pong", {});
+ this._pongReceivedTimestamp = dayjs();
+ // It is required to anonymous function since get this scope in checkAlive.
+ setTimeout(
+ () => this._checkAlive(this._pongReceivedTimestamp),
+ this._heartbeatInterval,
+ );
+ });
+ client.on("open", () => {
+ this.emit("connect", {});
+ this._channel();
+ // Call first ping event.
+ setTimeout(() => {
+ client.ping("");
+ }, 10000);
+ });
+ client.on("message", (data: WS.Data, isBinary: boolean) => {
+ this.parser.parse(data, isBinary, this._channelID);
+ });
+ client.on("error", (err: Error) => {
+ this.emit("error", err);
+ });
+ }
+
+ /**
+ * Set up parser when receive message.
+ */
+ private _setupParser() {
+ this.parser.on("update", (note: MisskeyAPI.Entity.Note) => {
+ this.emit(
+ "update",
+ this._converter.note(note, this.baseUrlToHost(this.url)),
+ );
+ });
+ this.parser.on(
+ "notification",
+ (notification: MisskeyAPI.Entity.Notification) => {
+ this.emit(
+ "notification",
+ this._converter.notification(
+ notification,
+ this.baseUrlToHost(this.url),
+ ),
+ );
+ },
+ );
+ this.parser.on("conversation", (note: MisskeyAPI.Entity.Note) => {
+ this.emit(
+ "conversation",
+ this._converter.noteToConversation(note, this.baseUrlToHost(this.url)),
+ );
+ });
+ this.parser.on("error", (err: Error) => {
+ this.emit("parser-error", err);
+ });
+ }
+
+ /**
+ * Call ping and wait to pong.
+ */
+ private _checkAlive(timestamp: Dayjs) {
+ const now: Dayjs = dayjs();
+ // Block multiple calling, if multiple pong event occur.
+ // It the duration is less than interval, through ping.
+ if (
+ now.diff(timestamp) > this._heartbeatInterval - 1000 &&
+ !this._connectionClosed
+ ) {
+ // Skip ping when client is connecting.
+ // https://github.com/websockets/ws/blob/7.2.1/lib/websocket.js#L289
+ if (this._client && this._client.readyState !== WS.CONNECTING) {
+ this._pongWaiting = true;
+ this._client.ping("");
+ setTimeout(() => {
+ if (this._pongWaiting) {
+ this._pongWaiting = false;
+ this._reconnect();
+ }
+ }, 10000);
+ }
+ }
+ }
+}
+
+/**
+ * Parser
+ * This class provides parser for websocket message.
+ */
+export class Parser extends EventEmitter {
+ /**
+ * @param message Message body of websocket.
+ * @param channelID Parse only messages which has same channelID.
+ */
+ public parse(data: WS.Data, isBinary: boolean, channelID: string) {
+ const message = isBinary ? data : data.toString();
+ if (typeof message !== "string") {
+ this.emit("heartbeat", {});
+ return;
+ }
+
+ if (message === "") {
+ this.emit("heartbeat", {});
+ return;
+ }
+
+ let obj: {
+ type: string;
+ body: {
+ id: string;
+ type: string;
+ body: any;
+ };
+ };
+ let body: {
+ id: string;
+ type: string;
+ body: any;
+ };
+
+ try {
+ obj = JSON.parse(message);
+ if (obj.type !== "channel") {
+ return;
+ }
+ if (!obj.body) {
+ return;
+ }
+ body = obj.body;
+ if (body.id !== channelID) {
+ return;
+ }
+ } catch (err) {
+ this.emit(
+ "error",
+ new Error(
+ `Error parsing websocket reply: ${message}, error message: ${err}`,
+ ),
+ );
+ return;
+ }
+
+ switch (body.type) {
+ case "note":
+ this.emit("update", body.body as MisskeyAPI.Entity.Note);
+ break;
+ case "notification":
+ this.emit("notification", body.body as MisskeyAPI.Entity.Notification);
+ break;
+ case "mention": {
+ const note = body.body as MisskeyAPI.Entity.Note;
+ if (note.visibility === "specified") {
+ this.emit("conversation", note);
+ }
+ break;
+ }
+ // When renote and followed event, the same notification will be received.
+ case "renote":
+ case "followed":
+ case "follow":
+ case "unfollow":
+ case "receiveFollowRequest":
+ case "meUpdated":
+ case "readAllNotifications":
+ case "readAllUnreadSpecifiedNotes":
+ case "readAllAntennas":
+ case "readAllUnreadMentions":
+ case "unreadNotification":
+ // Ignore these events
+ break;
+ default:
+ this.emit(
+ "error",
+ new Error(`Unknown event has received: ${JSON.stringify(body)}`),
+ );
+ break;
+ }
+ }
+}
diff --git a/packages/megalodon/src/notification.ts b/packages/megalodon/src/notification.ts
new file mode 100644
index 0000000000..84cd23e40d
--- /dev/null
+++ b/packages/megalodon/src/notification.ts
@@ -0,0 +1,14 @@
+import Entity from "./entity";
+
+namespace NotificationType {
+ export const Follow: Entity.NotificationType = "follow";
+ export const Favourite: Entity.NotificationType = "favourite";
+ export const Reblog: Entity.NotificationType = "reblog";
+ export const Mention: Entity.NotificationType = "mention";
+ export const Reaction: Entity.NotificationType = "reaction";
+ export const FollowRequest: Entity.NotificationType = "follow_request";
+ export const Status: Entity.NotificationType = "status";
+ export const Poll: Entity.NotificationType = "poll";
+}
+
+export default NotificationType;
diff --git a/packages/megalodon/src/oauth.ts b/packages/megalodon/src/oauth.ts
new file mode 100644
index 0000000000..f0df721f0a
--- /dev/null
+++ b/packages/megalodon/src/oauth.ts
@@ -0,0 +1,123 @@
+/**
+ * OAuth
+ * Response data when oauth request.
+ **/
+namespace OAuth {
+ export type AppDataFromServer = {
+ id: string;
+ name: string;
+ website: string | null;
+ redirect_uri: string;
+ client_id: string;
+ client_secret: string;
+ };
+
+ export type TokenDataFromServer = {
+ access_token: string;
+ token_type: string;
+ scope: string;
+ created_at: number;
+ expires_in: number | null;
+ refresh_token: string | null;
+ };
+
+ export class AppData {
+ public url: string | null;
+ public session_token: string | null;
+ constructor(
+ public id: string,
+ public name: string,
+ public website: string | null,
+ public redirect_uri: string,
+ public client_id: string,
+ public client_secret: string,
+ ) {
+ this.url = null;
+ this.session_token = null;
+ }
+
+ /**
+ * Serialize raw application data from server
+ * @param raw from server
+ */
+ static from(raw: AppDataFromServer) {
+ return new this(
+ raw.id,
+ raw.name,
+ raw.website,
+ raw.redirect_uri,
+ raw.client_id,
+ raw.client_secret,
+ );
+ }
+
+ get redirectUri() {
+ return this.redirect_uri;
+ }
+ get clientId() {
+ return this.client_id;
+ }
+ get clientSecret() {
+ return this.client_secret;
+ }
+ }
+
+ export class TokenData {
+ public _scope: string;
+ constructor(
+ public access_token: string,
+ public token_type: string,
+ scope: string,
+ public created_at: number,
+ public expires_in: number | null = null,
+ public refresh_token: string | null = null,
+ ) {
+ this._scope = scope;
+ }
+
+ /**
+ * Serialize raw token data from server
+ * @param raw from server
+ */
+ static from(raw: TokenDataFromServer) {
+ return new this(
+ raw.access_token,
+ raw.token_type,
+ raw.scope,
+ raw.created_at,
+ raw.expires_in,
+ raw.refresh_token,
+ );
+ }
+
+ /**
+ * OAuth Aceess Token
+ */
+ get accessToken() {
+ return this.access_token;
+ }
+ get tokenType() {
+ return this.token_type;
+ }
+ get scope() {
+ return this._scope;
+ }
+ /**
+ * Application ID
+ */
+ get createdAt() {
+ return this.created_at;
+ }
+ get expiresIn() {
+ return this.expires_in;
+ }
+ /**
+ * OAuth Refresh Token
+ */
+ get refreshToken() {
+ return this.refresh_token;
+ }
+ }
+}
+
+export default OAuth;
diff --git a/packages/megalodon/src/parser.ts b/packages/megalodon/src/parser.ts
new file mode 100644
index 0000000000..2ddf2ac2e6
--- /dev/null
+++ b/packages/megalodon/src/parser.ts
@@ -0,0 +1,94 @@
+import { EventEmitter } from "events";
+import Entity from "./entity";
+
+/**
+ * Parser
+ * Parse response data in streaming.
+ **/
+export class Parser extends EventEmitter {
+ private message: string;
+
+ constructor() {
+ super();
+ this.message = "";
+ }
+
+ public parse(chunk: string) {
+ // skip heartbeats
+ if (chunk === ":thump\n") {
+ this.emit("heartbeat", {});
+ return;
+ }
+
+ this.message += chunk;
+ chunk = this.message;
+
+ const size: number = chunk.length;
+ let start = 0;
+ let offset = 0;
+ let curr: string | undefined;
+ let next: string | undefined;
+
+ while (offset < size) {
+ curr = chunk[offset];
+ next = chunk[offset + 1];
+
+ if (curr === "\n" && next === "\n") {
+ const piece: string = chunk.slice(start, offset);
+
+ offset += 2;
+ start = offset;
+
+ if (!piece.length) continue; // empty object
+
+ const root: Array<string> = piece.split("\n");
+
+ // should never happen, as long as mastodon doesn't change API messages
+ if (root.length !== 2) continue;
+
+ // remove event and data markers
+ const event: string = root[0].substr(7);
+ const data: string = root[1].substr(6);
+
+ let jsonObj = {};
+ try {
+ jsonObj = JSON.parse(data);
+ } catch (err) {
+ // delete event does not have json object
+ if (event !== "delete") {
+ this.emit(
+ "error",
+ new Error(
+ `Error parsing API reply: '${piece}', error message: '${err}'`,
+ ),
+ );
+ continue;
+ }
+ }
+ switch (event) {
+ case "update":
+ this.emit("update", jsonObj as Entity.Status);
+ break;
+ case "notification":
+ this.emit("notification", jsonObj as Entity.Notification);
+ break;
+ case "conversation":
+ this.emit("conversation", jsonObj as Entity.Conversation);
+ break;
+ case "delete":
+ // When delete, data is an ID of the deleted status
+ this.emit("delete", data);
+ break;
+ default:
+ this.emit(
+ "error",
+ new Error(`Unknown event has received: ${event}`),
+ );
+ continue;
+ }
+ }
+ offset++;
+ }
+ this.message = chunk.slice(start, size);
+ }
+}
diff --git a/packages/megalodon/src/proxy_config.ts b/packages/megalodon/src/proxy_config.ts
new file mode 100644
index 0000000000..fadbcf084e
--- /dev/null
+++ b/packages/megalodon/src/proxy_config.ts
@@ -0,0 +1,92 @@
+import { HttpsProxyAgent, HttpsProxyAgentOptions } from "https-proxy-agent";
+import { SocksProxyAgent, SocksProxyAgentOptions } from "socks-proxy-agent";
+
+export type ProxyConfig = {
+ host: string;
+ port: number;
+ auth?: {
+ username: string;
+ password: string;
+ };
+ protocol:
+ | "http"
+ | "https"
+ | "socks4"
+ | "socks4a"
+ | "socks5"
+ | "socks5h"
+ | "socks";
+};
+
+class ProxyProtocolError extends Error {}
+
+const proxyAgent = (
+ proxyConfig: ProxyConfig,
+): HttpsProxyAgent | SocksProxyAgent => {
+ switch (proxyConfig.protocol) {
+ case "http": {
+ let options: HttpsProxyAgentOptions = {
+ host: proxyConfig.host,
+ port: proxyConfig.port,
+ secureProxy: false,
+ };
+ if (proxyConfig.auth) {
+ options = Object.assign(options, {
+ auth: `${proxyConfig.auth.username}:${proxyConfig.auth.password}`,
+ });
+ }
+ const httpsAgent = new HttpsProxyAgent(options);
+ return httpsAgent;
+ }
+ case "https": {
+ let options: HttpsProxyAgentOptions = {
+ host: proxyConfig.host,
+ port: proxyConfig.port,
+ secureProxy: true,
+ };
+ if (proxyConfig.auth) {
+ options = Object.assign(options, {
+ auth: `${proxyConfig.auth.username}:${proxyConfig.auth.password}`,
+ });
+ }
+ const httpsAgent = new HttpsProxyAgent(options);
+ return httpsAgent;
+ }
+ case "socks4":
+ case "socks4a": {
+ let options: SocksProxyAgentOptions = {
+ type: 4,
+ hostname: proxyConfig.host,
+ port: proxyConfig.port,
+ };
+ if (proxyConfig.auth) {
+ options = Object.assign(options, {
+ userId: proxyConfig.auth.username,
+ password: proxyConfig.auth.password,
+ });
+ }
+ const socksAgent = new SocksProxyAgent(options);
+ return socksAgent;
+ }
+ case "socks5":
+ case "socks5h":
+ case "socks": {
+ let options: SocksProxyAgentOptions = {
+ type: 5,
+ hostname: proxyConfig.host,
+ port: proxyConfig.port,
+ };
+ if (proxyConfig.auth) {
+ options = Object.assign(options, {
+ userId: proxyConfig.auth.username,
+ password: proxyConfig.auth.password,
+ });
+ }
+ const socksAgent = new SocksProxyAgent(options);
+ return socksAgent;
+ }
+ default:
+ throw new ProxyProtocolError("protocol is not accepted");
+ }
+};
+export default proxyAgent;
diff --git a/packages/megalodon/src/response.ts b/packages/megalodon/src/response.ts
new file mode 100644
index 0000000000..13fd8ab574
--- /dev/null
+++ b/packages/megalodon/src/response.ts
@@ -0,0 +1,8 @@
+type Response<T = any> = {
+ data: T;
+ status: number;
+ statusText: string;
+ headers: any;
+};
+
+export default Response;
diff --git a/packages/megalodon/test/integration/megalodon.spec.ts b/packages/megalodon/test/integration/megalodon.spec.ts
new file mode 100644
index 0000000000..8964535509
--- /dev/null
+++ b/packages/megalodon/test/integration/megalodon.spec.ts
@@ -0,0 +1,27 @@
+import { detector } from '../../src/index'
+
+describe('detector', () => {
+ describe('mastodon', () => {
+ const url = 'https://fedibird.com'
+ it('should be mastodon', async () => {
+ const mastodon = await detector(url)
+ expect(mastodon).toEqual('mastodon')
+ })
+ })
+
+ describe('pleroma', () => {
+ const url = 'https://pleroma.soykaf.com'
+ it('should be pleroma', async () => {
+ const pleroma = await detector(url)
+ expect(pleroma).toEqual('pleroma')
+ })
+ })
+
+ describe('misskey', () => {
+ const url = 'https://misskey.io'
+ it('should be misskey', async () => {
+ const misskey = await detector(url)
+ expect(misskey).toEqual('misskey')
+ })
+ })
+})
diff --git a/packages/megalodon/test/integration/misskey.spec.ts b/packages/megalodon/test/integration/misskey.spec.ts
new file mode 100644
index 0000000000..0ec1288428
--- /dev/null
+++ b/packages/megalodon/test/integration/misskey.spec.ts
@@ -0,0 +1,204 @@
+import MisskeyEntity from '@/misskey/entity'
+import MisskeyNotificationType from '@/misskey/notification'
+import Misskey from '@/misskey'
+import MegalodonNotificationType from '@/notification'
+import axios, { AxiosResponse } from 'axios'
+
+jest.mock('axios')
+
+const user: MisskeyEntity.User = {
+ id: '1',
+ name: 'test_user',
+ username: 'TestUser',
+ host: 'misskey.io',
+ avatarUrl: 'https://example.com/icon.png',
+ avatarColor: '#000000',
+ emojis: []
+}
+
+const note: MisskeyEntity.Note = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: '1',
+ user: user,
+ text: 'hogehoge',
+ cw: null,
+ visibility: 'public',
+ renoteCount: 0,
+ repliesCount: 0,
+ reactions: {},
+ emojis: [],
+ fileIds: [],
+ files: [],
+ replyId: null,
+ renoteId: null
+}
+
+const follow: MisskeyEntity.Notification = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: user.id,
+ user: user,
+ type: MisskeyNotificationType.Follow
+}
+
+const mention: MisskeyEntity.Notification = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: user.id,
+ user: user,
+ type: MisskeyNotificationType.Mention,
+ note: note
+}
+
+const reply: MisskeyEntity.Notification = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: user.id,
+ user: user,
+ type: MisskeyNotificationType.Reply,
+ note: note
+}
+
+const renote: MisskeyEntity.Notification = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: user.id,
+ user: user,
+ type: MisskeyNotificationType.Renote,
+ note: note
+}
+
+const quote: MisskeyEntity.Notification = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: user.id,
+ user: user,
+ type: MisskeyNotificationType.Quote,
+ note: note
+}
+
+const reaction: MisskeyEntity.Notification = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: user.id,
+ user: user,
+ type: MisskeyNotificationType.Reaction,
+ note: note,
+ reaction: '♥'
+}
+
+const pollVote: MisskeyEntity.Notification = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: user.id,
+ user: user,
+ type: MisskeyNotificationType.PollEnded,
+ note: note
+}
+
+const receiveFollowRequest: MisskeyEntity.Notification = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: user.id,
+ user: user,
+ type: MisskeyNotificationType.ReceiveFollowRequest
+}
+
+const followRequestAccepted: MisskeyEntity.Notification = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: user.id,
+ user: user,
+ type: MisskeyNotificationType.FollowRequestAccepted
+}
+
+const groupInvited: MisskeyEntity.Notification = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: user.id,
+ user: user,
+ type: MisskeyNotificationType.GroupInvited
+}
+
+;(axios.CancelToken.source as any).mockImplementation(() => {
+ return {
+ token: {
+ throwIfRequested: () => {},
+ promise: {
+ then: () => {},
+ catch: () => {}
+ }
+ }
+ }
+})
+
+describe('getNotifications', () => {
+ const client = new Misskey('http://localhost', 'sample token')
+ const cases: Array<{ event: MisskeyEntity.Notification; expected: Entity.NotificationType; title: string }> = [
+ {
+ event: follow,
+ expected: MegalodonNotificationType.Follow,
+ title: 'follow'
+ },
+ {
+ event: mention,
+ expected: MegalodonNotificationType.Mention,
+ title: 'mention'
+ },
+ {
+ event: reply,
+ expected: MegalodonNotificationType.Mention,
+ title: 'reply'
+ },
+ {
+ event: renote,
+ expected: MegalodonNotificationType.Reblog,
+ title: 'renote'
+ },
+ {
+ event: quote,
+ expected: MegalodonNotificationType.Reblog,
+ title: 'quote'
+ },
+ {
+ event: reaction,
+ expected: MegalodonNotificationType.Reaction,
+ title: 'reaction'
+ },
+ {
+ event: pollVote,
+ expected: MegalodonNotificationType.Poll,
+ title: 'pollVote'
+ },
+ {
+ event: receiveFollowRequest,
+ expected: MegalodonNotificationType.FollowRequest,
+ title: 'receiveFollowRequest'
+ },
+ {
+ event: followRequestAccepted,
+ expected: MegalodonNotificationType.Follow,
+ title: 'followRequestAccepted'
+ },
+ {
+ event: groupInvited,
+ expected: MisskeyNotificationType.GroupInvited,
+ title: 'groupInvited'
+ }
+ ]
+ cases.forEach(c => {
+ it(`should be ${c.title} event`, async () => {
+ const mockResponse: AxiosResponse<Array<MisskeyEntity.Notification>> = {
+ data: [c.event],
+ status: 200,
+ statusText: '200OK',
+ headers: {},
+ config: {}
+ }
+ ;(axios.post as any).mockResolvedValue(mockResponse)
+ const res = await client.getNotifications()
+ expect(res.data[0].type).toEqual(c.expected)
+ })
+ })
+})
diff --git a/packages/megalodon/test/unit/misskey/api_client.spec.ts b/packages/megalodon/test/unit/misskey/api_client.spec.ts
new file mode 100644
index 0000000000..7cf33b983d
--- /dev/null
+++ b/packages/megalodon/test/unit/misskey/api_client.spec.ts
@@ -0,0 +1,233 @@
+import MisskeyAPI from '@/misskey/api_client'
+import MegalodonEntity from '@/entity'
+import MisskeyEntity from '@/misskey/entity'
+import MegalodonNotificationType from '@/notification'
+import MisskeyNotificationType from '@/misskey/notification'
+
+const user: MisskeyEntity.User = {
+ id: '1',
+ name: 'test_user',
+ username: 'TestUser',
+ host: 'misskey.io',
+ avatarUrl: 'https://example.com/icon.png',
+ avatarColor: '#000000',
+ emojis: []
+}
+
+const converter: MisskeyAPI.Converter = new MisskeyAPI.Converter("https://example.com")
+
+describe('api_client', () => {
+ describe('notification', () => {
+ describe('encode', () => {
+ it('megalodon notification type should be encoded to misskey notification type', () => {
+ const cases: Array<{ src: MegalodonEntity.NotificationType; dist: MisskeyEntity.NotificationType }> = [
+ {
+ src: MegalodonNotificationType.Follow,
+ dist: MisskeyNotificationType.Follow
+ },
+ {
+ src: MegalodonNotificationType.Mention,
+ dist: MisskeyNotificationType.Reply
+ },
+ {
+ src: MegalodonNotificationType.Favourite,
+ dist: MisskeyNotificationType.Reaction
+ },
+ {
+ src: MegalodonNotificationType.Reaction,
+ dist: MisskeyNotificationType.Reaction
+ },
+ {
+ src: MegalodonNotificationType.Reblog,
+ dist: MisskeyNotificationType.Renote
+ },
+ {
+ src: MegalodonNotificationType.Poll,
+ dist: MisskeyNotificationType.PollEnded
+ },
+ {
+ src: MegalodonNotificationType.FollowRequest,
+ dist: MisskeyNotificationType.ReceiveFollowRequest
+ }
+ ]
+ cases.forEach(c => {
+ expect(converter.encodeNotificationType(c.src)).toEqual(c.dist)
+ })
+ })
+ })
+ describe('decode', () => {
+ it('misskey notification type should be decoded to megalodon notification type', () => {
+ const cases: Array<{ src: MisskeyEntity.NotificationType; dist: MegalodonEntity.NotificationType }> = [
+ {
+ src: MisskeyNotificationType.Follow,
+ dist: MegalodonNotificationType.Follow
+ },
+ {
+ src: MisskeyNotificationType.Mention,
+ dist: MegalodonNotificationType.Mention
+ },
+ {
+ src: MisskeyNotificationType.Reply,
+ dist: MegalodonNotificationType.Mention
+ },
+ {
+ src: MisskeyNotificationType.Renote,
+ dist: MegalodonNotificationType.Reblog
+ },
+ {
+ src: MisskeyNotificationType.Quote,
+ dist: MegalodonNotificationType.Reblog
+ },
+ {
+ src: MisskeyNotificationType.Reaction,
+ dist: MegalodonNotificationType.Reaction
+ },
+ {
+ src: MisskeyNotificationType.PollEnded,
+ dist: MegalodonNotificationType.Poll
+ },
+ {
+ src: MisskeyNotificationType.ReceiveFollowRequest,
+ dist: MegalodonNotificationType.FollowRequest
+ },
+ {
+ src: MisskeyNotificationType.FollowRequestAccepted,
+ dist: MegalodonNotificationType.Follow
+ }
+ ]
+ cases.forEach(c => {
+ expect(converter.decodeNotificationType(c.src)).toEqual(c.dist)
+ })
+ })
+ })
+ })
+ describe('reactions', () => {
+ it('should be mapped', () => {
+ const misskeyReactions = [
+ {
+ id: '1',
+ createdAt: '2020-04-21T13:04:13.968Z',
+ user: {
+ id: '81u70uwsja',
+ name: 'h3poteto',
+ username: 'h3poteto',
+ host: null,
+ avatarUrl: 'https://s3.arkjp.net/misskey/thumbnail-63807d97-20ca-40ba-9493-179aa48065c1.png',
+ avatarColor: 'rgb(146,189,195)',
+ emojis: []
+ },
+ type: '❤'
+ },
+ {
+ id: '2',
+ createdAt: '2020-04-21T13:04:13.968Z',
+ user: {
+ id: '81u70uwsja',
+ name: 'h3poteto',
+ username: 'h3poteto',
+ host: null,
+ avatarUrl: 'https://s3.arkjp.net/misskey/thumbnail-63807d97-20ca-40ba-9493-179aa48065c1.png',
+ avatarColor: 'rgb(146,189,195)',
+ emojis: []
+ },
+ type: '❤'
+ },
+ {
+ id: '3',
+ createdAt: '2020-04-21T13:04:13.968Z',
+ user: {
+ id: '81u70uwsja',
+ name: 'h3poteto',
+ username: 'h3poteto',
+ host: null,
+ avatarUrl: 'https://s3.arkjp.net/misskey/thumbnail-63807d97-20ca-40ba-9493-179aa48065c1.png',
+ avatarColor: 'rgb(146,189,195)',
+ emojis: []
+ },
+ type: '☺'
+ },
+ {
+ id: '4',
+ createdAt: '2020-04-21T13:04:13.968Z',
+ user: {
+ id: '81u70uwsja',
+ name: 'h3poteto',
+ username: 'h3poteto',
+ host: null,
+ avatarUrl: 'https://s3.arkjp.net/misskey/thumbnail-63807d97-20ca-40ba-9493-179aa48065c1.png',
+ avatarColor: 'rgb(146,189,195)',
+ emojis: []
+ },
+ type: '❤'
+ }
+ ]
+
+ const reactions = converter.reactions(misskeyReactions)
+ expect(reactions).toEqual([
+ {
+ count: 3,
+ me: false,
+ name: '❤'
+ },
+ {
+ count: 1,
+ me: false,
+ name: '☺'
+ }
+ ])
+ })
+ })
+
+ describe('status', () => {
+ describe('plain content', () => {
+ it('should be exported plain content and html content', () => {
+ const plainContent = 'hoge\nfuga\nfuga'
+ const content = 'hoge<br>fuga<br>fuga'
+ const note: MisskeyEntity.Note = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: '1',
+ user: user,
+ text: plainContent,
+ cw: null,
+ visibility: 'public',
+ renoteCount: 0,
+ repliesCount: 0,
+ reactions: {},
+ emojis: [],
+ fileIds: [],
+ files: [],
+ replyId: null,
+ renoteId: null
+ }
+ const megalodonStatus = converter.note(note, user.host || 'misskey.io')
+ expect(megalodonStatus.plain_content).toEqual(plainContent)
+ expect(megalodonStatus.content).toEqual(content)
+ })
+ it('html tags should be escaped', () => {
+ const plainContent = '<p>hoge\nfuga\nfuga<p>'
+ const content = '&lt;p&gt;hoge<br>fuga<br>fuga&lt;p&gt;'
+ const note: MisskeyEntity.Note = {
+ id: '1',
+ createdAt: '2021-02-01T01:49:29',
+ userId: '1',
+ user: user,
+ text: plainContent,
+ cw: null,
+ visibility: 'public',
+ renoteCount: 0,
+ repliesCount: 0,
+ reactions: {},
+ emojis: [],
+ fileIds: [],
+ files: [],
+ replyId: null,
+ renoteId: null
+ }
+ const megalodonStatus = converter.note(note, user.host || 'misskey.io')
+ expect(megalodonStatus.plain_content).toEqual(plainContent)
+ expect(megalodonStatus.content).toEqual(content)
+ })
+ })
+ })
+})
diff --git a/packages/megalodon/test/unit/parser.spec.ts b/packages/megalodon/test/unit/parser.spec.ts
new file mode 100644
index 0000000000..5174a647c6
--- /dev/null
+++ b/packages/megalodon/test/unit/parser.spec.ts
@@ -0,0 +1,152 @@
+import { Parser } from '@/parser'
+import Entity from '@/entity'
+
+const account: Entity.Account = {
+ id: '1',
+ username: 'h3poteto',
+ acct: 'h3poteto@pleroma.io',
+ display_name: 'h3poteto',
+ locked: false,
+ created_at: '2019-03-26T21:30:32',
+ followers_count: 10,
+ following_count: 10,
+ statuses_count: 100,
+ note: 'engineer',
+ url: 'https://pleroma.io',
+ avatar: '',
+ avatar_static: '',
+ header: '',
+ header_static: '',
+ emojis: [],
+ moved: null,
+ fields: [],
+ bot: false
+}
+
+const status: Entity.Status = {
+ id: '1',
+ uri: 'http://example.com',
+ url: 'http://example.com',
+ account: account,
+ in_reply_to_id: null,
+ in_reply_to_account_id: null,
+ reblog: null,
+ content: 'hoge',
+ plain_content: 'hoge',
+ created_at: '2019-03-26T21:40:32',
+ emojis: [],
+ replies_count: 0,
+ reblogs_count: 0,
+ favourites_count: 0,
+ reblogged: null,
+ favourited: null,
+ muted: null,
+ sensitive: false,
+ spoiler_text: '',
+ visibility: 'public',
+ media_attachments: [],
+ mentions: [],
+ tags: [],
+ card: null,
+ poll: null,
+ application: {
+ name: 'Web'
+ } as Entity.Application,
+ language: null,
+ pinned: null,
+ reactions: [],
+ bookmarked: false,
+ quote: null
+}
+
+const notification: Entity.Notification = {
+ id: '1',
+ account: account,
+ status: status,
+ type: 'favourite',
+ created_at: '2019-04-01T17:01:32'
+}
+
+const conversation: Entity.Conversation = {
+ id: '1',
+ accounts: [account],
+ last_status: status,
+ unread: true
+}
+
+describe('Parser', () => {
+ let parser: Parser
+
+ beforeEach(() => {
+ parser = new Parser()
+ })
+
+ describe('parse', () => {
+ describe('message is heartbeat', () => {
+ const message: string = ':thump\n'
+ it('should be called', () => {
+ const spy = jest.fn()
+ parser.on('heartbeat', spy)
+ parser.parse(message)
+ expect(spy).toHaveBeenLastCalledWith({})
+ })
+ })
+
+ describe('message is not json', () => {
+ describe('event is delete', () => {
+ const message = `event: delete\ndata: 12asdf34\n\n`
+ it('should be called', () => {
+ const spy = jest.fn()
+ parser.once('delete', spy)
+ parser.parse(message)
+ expect(spy).toHaveBeenCalledWith('12asdf34')
+ })
+ })
+
+ describe('event is not delete', () => {
+ const message = `event: event\ndata: 12asdf34\n\n`
+ it('should be error', () => {
+ const error = jest.fn()
+ const deleted = jest.fn()
+ parser.once('error', error)
+ parser.once('delete', deleted)
+ parser.parse(message)
+ expect(error).toHaveBeenCalled()
+ expect(deleted).not.toHaveBeenCalled()
+ })
+ })
+ })
+
+ describe('message is json', () => {
+ describe('event is update', () => {
+ const message = `event: update\ndata: ${JSON.stringify(status)}\n\n`
+ it('should be called', () => {
+ const spy = jest.fn()
+ parser.once('update', spy)
+ parser.parse(message)
+ expect(spy).toHaveBeenCalledWith(status)
+ })
+ })
+
+ describe('event is notification', () => {
+ const message = `event: notification\ndata: ${JSON.stringify(notification)}\n\n`
+ it('should be called', () => {
+ const spy = jest.fn()
+ parser.once('notification', spy)
+ parser.parse(message)
+ expect(spy).toHaveBeenCalledWith(notification)
+ })
+ })
+
+ describe('event is conversation', () => {
+ const message = `event: conversation\ndata: ${JSON.stringify(conversation)}\n\n`
+ it('should be called', () => {
+ const spy = jest.fn()
+ parser.once('conversation', spy)
+ parser.parse(message)
+ expect(spy).toHaveBeenCalledWith(conversation)
+ })
+ })
+ })
+ })
+})
diff --git a/packages/megalodon/tsconfig.json b/packages/megalodon/tsconfig.json
new file mode 100644
index 0000000000..5a9bfbde9a
--- /dev/null
+++ b/packages/megalodon/tsconfig.json
@@ -0,0 +1,64 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
+ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+ "lib": ["es2021", "dom"], /* Specify library files to be included in the compilation. */
+ // "allowJs": true, /* Allow javascript files to be compiled. */
+ // "checkJs": true, /* Report errors in .js files. */
+ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+ "declaration": true, /* Generates corresponding '.d.ts' file. */
+ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
+ // "sourceMap": true, /* Generates corresponding '.map' file. */
+ // "outFile": "./", /* Concatenate and emit output to single file. */
+ "outDir": "./lib", /* Redirect output structure to the directory. */
+ "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+ // "composite": true, /* Enable project compilation */
+ "removeComments": true, /* Do not emit comments to output. */
+ // "noEmit": true, /* Do not emit outputs. */
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+ /* Strict Type-Checking Options */
+ "strict": true, /* Enable all strict type-checking options. */
+ "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
+ "strictNullChecks": true, /* Enable strict null checks. */
+ "strictFunctionTypes": true, /* Enable strict checking of function types. */
+ "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
+ "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
+ "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
+
+ /* Additional Checks */
+ "noUnusedLocals": false, /* Report errors on unused locals. */
+ "noUnusedParameters": true, /* Report errors on unused parameters. */
+ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+
+ /* Module Resolution Options */
+ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+ "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ "paths": {
+ "@*": ["src*"],
+ "~*": ["./*"]
+ }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+
+ /* Source Map Options */
+ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+ },
+ "include": ["./src", "./test"],
+ "exclude": ["node_modules", "example"]
+}
diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts
index f33ab1c33c..0f9254216a 100644
--- a/packages/sw/src/scripts/create-notification.ts
+++ b/packages/sw/src/scripts/create-notification.ts
@@ -250,7 +250,7 @@ export async function createEmptyNotification(): Promise<void> {
await globalThis.registration.showNotification(
(new URL(origin)).host,
{
- body: `Misskey v${_VERSION_}`,
+ body: `Sharkey v${_VERSION_}`,
silent: true,
badge: iconUrl('null'),
tag: 'read_notification',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 902102d84e..42faf9301c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -185,6 +185,9 @@ importers:
fastify:
specifier: 4.23.2
version: 4.23.2
+ fastify-multer:
+ specifier: ^2.0.3
+ version: 2.0.3
feed:
specifier: 4.2.2
version: 4.2.2
@@ -236,6 +239,9 @@ importers:
jsrsasign:
specifier: 10.8.6
version: 10.8.6
+ megalodon:
+ specifier: workspace:*
+ version: link:../megalodon
meilisearch:
specifier: 0.34.2
version: 0.34.2
@@ -999,6 +1005,124 @@ importers:
specifier: 1.8.11
version: 1.8.11(typescript@5.2.2)
+ packages/megalodon:
+ dependencies:
+ '@types/oauth':
+ specifier: ^0.9.0
+ version: 0.9.2
+ '@types/ws':
+ specifier: ^8.5.4
+ version: 8.5.5
+ async-lock:
+ specifier: 1.4.0
+ version: 1.4.0
+ axios:
+ specifier: 1.2.2
+ version: 1.2.2
+ dayjs:
+ specifier: ^1.11.7
+ version: 1.11.7
+ form-data:
+ specifier: ^4.0.0
+ version: 4.0.0
+ https-proxy-agent:
+ specifier: ^5.0.1
+ version: 5.0.1
+ oauth:
+ specifier: ^0.10.0
+ version: 0.10.0
+ object-assign-deep:
+ specifier: ^0.4.0
+ version: 0.4.0
+ parse-link-header:
+ specifier: ^2.0.0
+ version: 2.0.0
+ socks-proxy-agent:
+ specifier: ^7.0.0
+ version: 7.0.0
+ typescript:
+ specifier: 4.9.4
+ version: 4.9.4
+ uuid:
+ specifier: ^9.0.0
+ version: 9.0.1
+ ws:
+ specifier: 8.12.0
+ version: 8.12.0
+ devDependencies:
+ '@types/async-lock':
+ specifier: 1.4.0
+ version: 1.4.0
+ '@types/core-js':
+ specifier: ^2.5.0
+ version: 2.5.0
+ '@types/form-data':
+ specifier: ^2.5.0
+ version: 2.5.0
+ '@types/jest':
+ specifier: ^29.4.0
+ version: 29.5.5
+ '@types/node':
+ specifier: 18.11.18
+ version: 18.11.18
+ '@types/object-assign-deep':
+ specifier: ^0.4.0
+ version: 0.4.0
+ '@types/parse-link-header':
+ specifier: ^2.0.0
+ version: 2.0.0
+ '@types/uuid':
+ specifier: ^9.0.0
+ version: 9.0.4
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^5.49.0
+ version: 5.49.0(@typescript-eslint/parser@5.49.0)(eslint@8.49.0)(typescript@4.9.4)
+ '@typescript-eslint/parser':
+ specifier: ^5.49.0
+ version: 5.49.0(eslint@8.49.0)(typescript@4.9.4)
+ eslint:
+ specifier: ^8.32.0
+ version: 8.49.0
+ eslint-config-prettier:
+ specifier: ^8.6.0
+ version: 8.6.0(eslint@8.49.0)
+ eslint-config-standard:
+ specifier: ^16.0.3
+ version: 16.0.3(eslint-plugin-import@2.28.1)(eslint-plugin-node@11.0.0)(eslint-plugin-promise@6.1.1)(eslint@8.49.0)
+ eslint-plugin-import:
+ specifier: ^2.27.5
+ version: 2.28.1(@typescript-eslint/parser@5.49.0)(eslint@8.49.0)
+ eslint-plugin-node:
+ specifier: ^11.0.0
+ version: 11.0.0(eslint@8.49.0)
+ eslint-plugin-prettier:
+ specifier: ^4.2.1
+ version: 4.2.1(eslint-config-prettier@8.6.0)(eslint@8.49.0)(prettier@2.8.8)
+ eslint-plugin-promise:
+ specifier: ^6.1.1
+ version: 6.1.1(eslint@8.49.0)
+ eslint-plugin-standard:
+ specifier: ^5.0.0
+ version: 5.0.0(eslint@8.49.0)
+ jest:
+ specifier: ^29.4.0
+ version: 29.7.0(@types/node@18.11.18)
+ jest-worker:
+ specifier: ^29.4.0
+ version: 29.7.0
+ lodash:
+ specifier: 4.17.21
+ version: 4.17.21
+ prettier:
+ specifier: ^2.8.3
+ version: 2.8.8
+ ts-jest:
+ specifier: ^29.0.5
+ version: 29.0.5(@babel/core@7.22.11)(jest@29.7.0)(typescript@4.9.4)
+ typedoc:
+ specifier: ^0.23.24
+ version: 0.23.24(typescript@4.9.4)
+
packages/misskey-js:
dependencies:
'@swc/cli':
@@ -1682,13 +1806,6 @@ packages:
tslib: 2.6.2
dev: false
- /@babel/code-frame@7.21.4:
- resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==}
- engines: {node: '>=6.9.0'}
- dependencies:
- '@babel/highlight': 7.22.5
- dev: true
-
/@babel/code-frame@7.22.13:
resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==}
engines: {node: '>=6.9.0'}
@@ -1697,13 +1814,6 @@ packages:
chalk: 2.4.2
dev: true
- /@babel/code-frame@7.22.5:
- resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==}
- engines: {node: '>=6.9.0'}
- dependencies:
- '@babel/highlight': 7.22.5
- dev: true
-
/@babel/compat-data@7.22.9:
resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==}
engines: {node: '>=6.9.0'}
@@ -1857,7 +1967,7 @@ packages:
'@babel/helper-module-imports': 7.22.5
'@babel/helper-simple-access': 7.22.5
'@babel/helper-split-export-declaration': 7.22.6
- '@babel/helper-validator-identifier': 7.22.5
+ '@babel/helper-validator-identifier': 7.22.15
dev: true
/@babel/helper-optimise-call-expression@7.22.5:
@@ -1963,15 +2073,6 @@ packages:
js-tokens: 4.0.0
dev: true
- /@babel/highlight@7.22.5:
- resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==}
- engines: {node: '>=6.9.0'}
- dependencies:
- '@babel/helper-validator-identifier': 7.22.5
- chalk: 2.4.2
- js-tokens: 4.0.0
- dev: true
-
/@babel/parser@7.21.8:
resolution: {integrity: sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==}
engines: {node: '>=6.0.0'}
@@ -1979,20 +2080,12 @@ packages:
dependencies:
'@babel/types': 7.22.5
- /@babel/parser@7.22.11:
- resolution: {integrity: sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==}
- engines: {node: '>=6.0.0'}
- hasBin: true
- dependencies:
- '@babel/types': 7.22.17
- dev: true
-
/@babel/parser@7.22.16:
resolution: {integrity: sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
- '@babel/types': 7.22.11
+ '@babel/types': 7.22.17
/@babel/parser@7.22.7:
resolution: {integrity: sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==}
@@ -3050,14 +3143,6 @@ packages:
- supports-color
dev: true
- /@babel/types@7.22.11:
- resolution: {integrity: sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==}
- engines: {node: '>=6.9.0'}
- dependencies:
- '@babel/helper-string-parser': 7.22.5
- '@babel/helper-validator-identifier': 7.22.5
- to-fast-properties: 2.0.0
-
/@babel/types@7.22.17:
resolution: {integrity: sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==}
engines: {node: '>=6.9.0'}
@@ -7476,7 +7561,7 @@ packages:
resolution: {integrity: sha512-xTEnpUKiV/bMyEsE5bT4oYA0x0Z/colMtxzUY8bKyPXBNLn/e0V4ZjBZkEhms0xE4pv9QsPfSRu9AWS4y5wGvA==}
engines: {node: '>=14'}
dependencies:
- '@babel/code-frame': 7.21.4
+ '@babel/code-frame': 7.22.13
'@babel/runtime': 7.21.0
'@types/aria-query': 5.0.1
aria-query: 5.1.3
@@ -7578,6 +7663,10 @@ packages:
resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==}
dev: true
+ /@types/async-lock@1.4.0:
+ resolution: {integrity: sha512-2+rYSaWrpdbQG3SA0LmMT6YxWLrI81AqpMlSkw3QtFc2HGDufkweQSn30Eiev7x9LL0oyFrBqk1PXOnB9IEgKg==}
+ dev: true
+
/@types/babel__core@7.20.0:
resolution: {integrity: sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==}
dependencies:
@@ -7671,6 +7760,10 @@ packages:
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
dev: true
+ /@types/core-js@2.5.0:
+ resolution: {integrity: sha512-qjkHL3wF0JMHMqgm/kmL8Pf8rIiqvueEiZ0g6NVTcBX1WN46GWDr+V5z+gsHUeL0n8TfAmXnYmF7ajsxmBp4PQ==}
+ dev: true
+
/@types/cross-spawn@6.0.2:
resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==}
dependencies:
@@ -7752,6 +7845,13 @@ packages:
'@types/node': 20.6.3
dev: true
+ /@types/form-data@2.5.0:
+ resolution: {integrity: sha512-23/wYiuckYYtFpL+4RPWiWmRQH2BjFuqCUi2+N3amB1a1Drv+i/byTrGvlLwRVLFNAZbwpbQ7JvTK+VCAPMbcg==}
+ deprecated: This is a stub types definition. form-data provides its own type definitions, so you do not need this installed.
+ dependencies:
+ form-data: 4.0.0
+ dev: true
+
/@types/glob@7.2.0:
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
dependencies:
@@ -7907,6 +8007,10 @@ packages:
resolution: {integrity: sha512-Mnq3O9Xz52exs3mlxMcQuA7/9VFe/dXcrgAyfjLkABIqxXKOgBRjyazTxUbjsxDa4BP7hhPliyjVTP9RDP14xg==}
dev: true
+ /@types/node@18.11.18:
+ resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==}
+ dev: true
+
/@types/node@18.17.15:
resolution: {integrity: sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==}
dev: true
@@ -7941,6 +8045,9 @@ packages:
resolution: {integrity: sha512-Nu3/abQ6yR9VlsCdX3aiGsWFkj6OJvJqDvg/36t8Gwf2mFXdBZXPDN3K+2yfeA6Lo2m1Q12F8Qil9TZ48nWhOQ==}
dependencies:
'@types/node': 20.6.3
+
+ /@types/object-assign-deep@0.4.0:
+ resolution: {integrity: sha512-3D0F3rHRNDc8cQSXNzwF1jBrJi28Mdrhc10ZLlqbJWDPYRWTTWB9Tc8JoKrgBvLKioXoPoHT6Uzf3s2F7akCUg==}
dev: true
/@types/offscreencanvas@2019.3.0:
@@ -7953,6 +8060,10 @@ packages:
requiresBuild: true
dev: false
+ /@types/parse-link-header@2.0.0:
+ resolution: {integrity: sha512-KbqcQLdRaawDOfXnwqr6nvhe1MV+Uv/Ww+ViSx7Ujgw9X5qCgObLP52B1ZSJqZD8FK1y/4o+bJQTUrZOynegcg==}
+ dev: true
+
/@types/pg@8.10.2:
resolution: {integrity: sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==}
dependencies:
@@ -8143,7 +8254,6 @@ packages:
resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==}
dependencies:
'@types/node': 20.6.3
- dev: true
/@types/yargs-parser@21.0.0:
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
@@ -8169,6 +8279,33 @@ packages:
dev: true
optional: true
+ /@typescript-eslint/eslint-plugin@5.49.0(@typescript-eslint/parser@5.49.0)(eslint@8.49.0)(typescript@4.9.4):
+ resolution: {integrity: sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^5.0.0
+ eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@typescript-eslint/parser': 5.49.0(eslint@8.49.0)(typescript@4.9.4)
+ '@typescript-eslint/scope-manager': 5.49.0
+ '@typescript-eslint/type-utils': 5.49.0(eslint@8.49.0)(typescript@4.9.4)
+ '@typescript-eslint/utils': 5.49.0(eslint@8.49.0)(typescript@4.9.4)
+ debug: 4.3.4(supports-color@8.1.1)
+ eslint: 8.49.0
+ ignore: 5.2.4
+ natural-compare-lite: 1.4.0
+ regexpp: 3.2.0
+ semver: 7.5.4
+ tsutils: 3.21.0(typescript@4.9.4)
+ typescript: 4.9.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/@typescript-eslint/eslint-plugin@6.7.2(@typescript-eslint/parser@6.7.2)(eslint@8.49.0)(typescript@5.2.2):
resolution: {integrity: sha512-ooaHxlmSgZTM6CHYAFRlifqh1OAr3PAQEwi7lhYhaegbnXrnh7CDcHmc3+ihhbQC7H0i4JF0psI5ehzkF6Yl6Q==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -8198,6 +8335,26 @@ packages:
- supports-color
dev: true
+ /@typescript-eslint/parser@5.49.0(eslint@8.49.0)(typescript@4.9.4):
+ resolution: {integrity: sha512-veDlZN9mUhGqU31Qiv2qEp+XrJj5fgZpJ8PW30sHU+j/8/e5ruAhLaVDAeznS7A7i4ucb/s8IozpDtt9NqCkZg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@typescript-eslint/scope-manager': 5.49.0
+ '@typescript-eslint/types': 5.49.0
+ '@typescript-eslint/typescript-estree': 5.49.0(typescript@4.9.4)
+ debug: 4.3.4(supports-color@8.1.1)
+ eslint: 8.49.0
+ typescript: 4.9.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/@typescript-eslint/parser@6.7.2(eslint@8.49.0)(typescript@5.2.2):
resolution: {integrity: sha512-KA3E4ox0ws+SPyxQf9iSI25R6b4Ne78ORhNHeVKrPQnoYsb9UhieoiRoJgrzgEeKGOXhcY1i8YtOeCHHTDa6Fw==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -8219,6 +8376,14 @@ packages:
- supports-color
dev: true
+ /@typescript-eslint/scope-manager@5.49.0:
+ resolution: {integrity: sha512-clpROBOiMIzpbWNxCe1xDK14uPZh35u4QaZO1GddilEzoCLAEz4szb51rBpdgurs5k2YzPtJeTEN3qVbG+LRUQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ '@typescript-eslint/types': 5.49.0
+ '@typescript-eslint/visitor-keys': 5.49.0
+ dev: true
+
/@typescript-eslint/scope-manager@6.7.2:
resolution: {integrity: sha512-bgi6plgyZjEqapr7u2mhxGR6E8WCzKNUFWNh6fkpVe9+yzRZeYtDTbsIBzKbcxI+r1qVWt6VIoMSNZ4r2A+6Yw==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -8227,6 +8392,26 @@ packages:
'@typescript-eslint/visitor-keys': 6.7.2
dev: true
+ /@typescript-eslint/type-utils@5.49.0(eslint@8.49.0)(typescript@4.9.4):
+ resolution: {integrity: sha512-eUgLTYq0tR0FGU5g1YHm4rt5H/+V2IPVkP0cBmbhRyEmyGe4XvJ2YJ6sYTmONfjmdMqyMLad7SB8GvblbeESZA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: '*'
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@typescript-eslint/typescript-estree': 5.49.0(typescript@4.9.4)
+ '@typescript-eslint/utils': 5.49.0(eslint@8.49.0)(typescript@4.9.4)
+ debug: 4.3.4(supports-color@8.1.1)
+ eslint: 8.49.0
+ tsutils: 3.21.0(typescript@4.9.4)
+ typescript: 4.9.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/@typescript-eslint/type-utils@6.7.2(eslint@8.49.0)(typescript@5.2.2):
resolution: {integrity: sha512-36F4fOYIROYRl0qj95dYKx6kybddLtsbmPIYNK0OBeXv2j9L5nZ17j9jmfy+bIDHKQgn2EZX+cofsqi8NPATBQ==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -8247,11 +8432,37 @@ packages:
- supports-color
dev: true
+ /@typescript-eslint/types@5.49.0:
+ resolution: {integrity: sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dev: true
+
/@typescript-eslint/types@6.7.2:
resolution: {integrity: sha512-flJYwMYgnUNDAN9/GAI3l8+wTmvTYdv64fcH8aoJK76Y+1FCZ08RtI5zDerM/FYT5DMkAc+19E4aLmd5KqdFyg==}
engines: {node: ^16.0.0 || >=18.0.0}
dev: true
+ /@typescript-eslint/typescript-estree@5.49.0(typescript@4.9.4):
+ resolution: {integrity: sha512-PBdx+V7deZT/3GjNYPVQv1Nc0U46dAHbIuOG8AZ3on3vuEKiPDwFE/lG1snN2eUB9IhF7EyF7K1hmTcLztNIsA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@typescript-eslint/types': 5.49.0
+ '@typescript-eslint/visitor-keys': 5.49.0
+ debug: 4.3.4(supports-color@8.1.1)
+ globby: 11.1.0
+ is-glob: 4.0.3
+ semver: 7.5.4
+ tsutils: 3.21.0(typescript@4.9.4)
+ typescript: 4.9.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/@typescript-eslint/typescript-estree@6.7.2(typescript@5.2.2):
resolution: {integrity: sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -8273,6 +8484,26 @@ packages:
- supports-color
dev: true
+ /@typescript-eslint/utils@5.49.0(eslint@8.49.0)(typescript@4.9.4):
+ resolution: {integrity: sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
+ dependencies:
+ '@types/json-schema': 7.0.12
+ '@types/semver': 7.5.2
+ '@typescript-eslint/scope-manager': 5.49.0
+ '@typescript-eslint/types': 5.49.0
+ '@typescript-eslint/typescript-estree': 5.49.0(typescript@4.9.4)
+ eslint: 8.49.0
+ eslint-scope: 5.1.1
+ eslint-utils: 3.0.0(eslint@8.49.0)
+ semver: 7.5.4
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+ dev: true
+
/@typescript-eslint/utils@6.7.2(eslint@8.49.0)(typescript@5.2.2):
resolution: {integrity: sha512-ZCcBJug/TS6fXRTsoTkgnsvyWSiXwMNiPzBUani7hDidBdj1779qwM1FIAmpH4lvlOZNF3EScsxxuGifjpLSWQ==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -8292,6 +8523,14 @@ packages:
- typescript
dev: true
+ /@typescript-eslint/visitor-keys@5.49.0:
+ resolution: {integrity: sha512-v9jBMjpNWyn8B6k/Mjt6VbUS4J1GvUlR4x3Y+ibnP1z7y7V4n0WRz+50DY6+Myj0UaXVSuUlHohO+eZ8IJEnkg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ '@typescript-eslint/types': 5.49.0
+ eslint-visitor-keys: 3.4.3
+ dev: true
+
/@typescript-eslint/visitor-keys@6.7.2:
resolution: {integrity: sha512-uVw9VIMFBUTz8rIeaUT3fFe8xIUx8r4ywAdlQv1ifH+6acn/XF8Y6rwJ7XNmkNMDrTW+7+vxFFPIF40nJCVsMQ==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -8443,7 +8682,7 @@ packages:
/@vue/compiler-core@3.3.4:
resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==}
dependencies:
- '@babel/parser': 7.22.7
+ '@babel/parser': 7.22.16
'@vue/shared': 3.3.4
estree-walker: 2.0.2
source-map-js: 1.0.2
@@ -8807,6 +9046,10 @@ packages:
engines: {node: '>= 6.0.0'}
dev: false
+ /append-field@1.0.0:
+ resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
+ dev: false
+
/aproba@2.0.0:
resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
dev: false
@@ -9063,6 +9306,10 @@ packages:
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
dev: true
+ /async-lock@1.4.0:
+ resolution: {integrity: sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==}
+ dev: false
+
/async-mutex@0.4.0:
resolution: {integrity: sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==}
dependencies:
@@ -9135,6 +9382,16 @@ packages:
- debug
dev: true
+ /axios@1.2.2:
+ resolution: {integrity: sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==}
+ dependencies:
+ follow-redirects: 1.15.2(debug@4.3.4)
+ form-data: 4.0.0
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+ dev: false
+
/b4a@1.6.4:
resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==}
@@ -9438,6 +9695,13 @@ packages:
node-releases: 2.0.13
update-browserslist-db: 1.0.11(browserslist@4.21.9)
+ /bs-logger@0.2.6:
+ resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
+ engines: {node: '>= 6'}
+ dependencies:
+ fast-json-stable-stringify: 2.1.0
+ dev: true
+
/bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
dependencies:
@@ -9607,7 +9871,7 @@ packages:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies:
function-bind: 1.1.1
- get-intrinsic: 1.2.0
+ get-intrinsic: 1.2.1
/callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
@@ -10108,6 +10372,16 @@ packages:
typedarray: 0.0.6
dev: true
+ /concat-stream@2.0.0:
+ resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
+ engines: {'0': node >= 6.0}
+ dependencies:
+ buffer-from: 1.1.2
+ inherits: 2.0.4
+ readable-stream: 3.6.0
+ typedarray: 0.0.6
+ dev: false
+
/config-chain@1.1.13:
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
dependencies:
@@ -10190,6 +10464,25 @@ packages:
readable-stream: 3.6.0
dev: false
+ /create-jest@29.7.0(@types/node@18.11.18):
+ resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ hasBin: true
+ dependencies:
+ '@jest/types': 29.6.3
+ chalk: 4.1.2
+ exit: 0.1.2
+ graceful-fs: 4.2.11
+ jest-config: 29.7.0(@types/node@18.11.18)
+ jest-util: 29.7.0
+ prompts: 2.4.2
+ transitivePeerDependencies:
+ - '@types/node'
+ - babel-plugin-macros
+ - supports-color
+ - ts-node
+ dev: true
+
/create-jest@29.7.0(@types/node@20.6.3):
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -10485,7 +10778,6 @@ packages:
/dayjs@1.11.7:
resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==}
- dev: true
/de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
@@ -10659,14 +10951,6 @@ packages:
engines: {node: '>=8'}
dev: true
- /define-properties@1.1.4:
- resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
- engines: {node: '>= 0.4'}
- dependencies:
- has-property-descriptors: 1.0.0
- object-keys: 1.1.1
- dev: true
-
/define-properties@1.2.0:
resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==}
engines: {node: '>= 0.4'}
@@ -11024,7 +11308,7 @@ packages:
resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==}
dependencies:
call-bind: 1.0.2
- get-intrinsic: 1.2.0
+ get-intrinsic: 1.2.1
has-symbols: 1.0.3
is-arguments: 1.1.1
is-map: 2.0.2
@@ -11195,6 +11479,29 @@ packages:
source-map: 0.6.1
dev: true
+ /eslint-config-prettier@8.6.0(eslint@8.49.0):
+ resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==}
+ hasBin: true
+ peerDependencies:
+ eslint: '>=7.0.0'
+ dependencies:
+ eslint: 8.49.0
+ dev: true
+
+ /eslint-config-standard@16.0.3(eslint-plugin-import@2.28.1)(eslint-plugin-node@11.0.0)(eslint-plugin-promise@6.1.1)(eslint@8.49.0):
+ resolution: {integrity: sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==}
+ peerDependencies:
+ eslint: ^7.12.1
+ eslint-plugin-import: ^2.22.1
+ eslint-plugin-node: ^11.1.0
+ eslint-plugin-promise: ^4.2.1 || ^5.0.0
+ dependencies:
+ eslint: 8.49.0
+ eslint-plugin-import: 2.28.1(@typescript-eslint/parser@5.49.0)(eslint@8.49.0)
+ eslint-plugin-node: 11.0.0(eslint@8.49.0)
+ eslint-plugin-promise: 6.1.1(eslint@8.49.0)
+ dev: true
+
/eslint-formatter-pretty@4.1.0:
resolution: {integrity: sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==}
engines: {node: '>=10'}
@@ -11219,6 +11526,35 @@ packages:
- supports-color
dev: true
+ /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.49.0)(eslint-import-resolver-node@0.3.7)(eslint@8.49.0):
+ resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ '@typescript-eslint/parser': '*'
+ eslint: '*'
+ eslint-import-resolver-node: '*'
+ eslint-import-resolver-typescript: '*'
+ eslint-import-resolver-webpack: '*'
+ peerDependenciesMeta:
+ '@typescript-eslint/parser':
+ optional: true
+ eslint:
+ optional: true
+ eslint-import-resolver-node:
+ optional: true
+ eslint-import-resolver-typescript:
+ optional: true
+ eslint-import-resolver-webpack:
+ optional: true
+ dependencies:
+ '@typescript-eslint/parser': 5.49.0(eslint@8.49.0)(typescript@4.9.4)
+ debug: 3.2.7(supports-color@5.5.0)
+ eslint: 8.49.0
+ eslint-import-resolver-node: 0.3.7
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.2)(eslint-import-resolver-node@0.3.7)(eslint@8.49.0):
resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
engines: {node: '>=4'}
@@ -11248,6 +11584,52 @@ packages:
- supports-color
dev: true
+ /eslint-plugin-es@3.0.1(eslint@8.49.0):
+ resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==}
+ engines: {node: '>=8.10.0'}
+ peerDependencies:
+ eslint: '>=4.19.1'
+ dependencies:
+ eslint: 8.49.0
+ eslint-utils: 2.1.0
+ regexpp: 3.2.0
+ dev: true
+
+ /eslint-plugin-import@2.28.1(@typescript-eslint/parser@5.49.0)(eslint@8.49.0):
+ resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ '@typescript-eslint/parser': '*'
+ eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
+ peerDependenciesMeta:
+ '@typescript-eslint/parser':
+ optional: true
+ dependencies:
+ '@typescript-eslint/parser': 5.49.0(eslint@8.49.0)(typescript@4.9.4)
+ array-includes: 3.1.6
+ array.prototype.findlastindex: 1.2.2
+ array.prototype.flat: 1.3.1
+ array.prototype.flatmap: 1.3.1
+ debug: 3.2.7(supports-color@5.5.0)
+ doctrine: 2.1.0
+ eslint: 8.49.0
+ eslint-import-resolver-node: 0.3.7
+ eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.49.0)(eslint-import-resolver-node@0.3.7)(eslint@8.49.0)
+ has: 1.0.3
+ is-core-module: 2.13.0
+ is-glob: 4.0.3
+ minimatch: 3.1.2
+ object.fromentries: 2.0.6
+ object.groupby: 1.0.0
+ object.values: 1.1.6
+ semver: 6.3.1
+ tsconfig-paths: 3.14.2
+ transitivePeerDependencies:
+ - eslint-import-resolver-typescript
+ - eslint-import-resolver-webpack
+ - supports-color
+ dev: true
+
/eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.7.2)(eslint@8.49.0):
resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==}
engines: {node: '>=4'}
@@ -11283,6 +11665,56 @@ packages:
- supports-color
dev: true
+ /eslint-plugin-node@11.0.0(eslint@8.49.0):
+ resolution: {integrity: sha512-chUs/NVID+sknFiJzxoN9lM7uKSOEta8GC8365hw1nDfwIPIjjpRSwwPvQanWv8dt/pDe9EV4anmVSwdiSndNg==}
+ engines: {node: '>=8.10.0'}
+ peerDependencies:
+ eslint: '>=5.16.0'
+ dependencies:
+ eslint: 8.49.0
+ eslint-plugin-es: 3.0.1(eslint@8.49.0)
+ eslint-utils: 2.1.0
+ ignore: 5.2.4
+ minimatch: 3.1.2
+ resolve: 1.22.3
+ semver: 6.3.1
+ dev: true
+
+ /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.6.0)(eslint@8.49.0)(prettier@2.8.8):
+ resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ eslint: '>=7.28.0'
+ eslint-config-prettier: '*'
+ prettier: '>=2.0.0'
+ peerDependenciesMeta:
+ eslint-config-prettier:
+ optional: true
+ dependencies:
+ eslint: 8.49.0
+ eslint-config-prettier: 8.6.0(eslint@8.49.0)
+ prettier: 2.8.8
+ prettier-linter-helpers: 1.0.0
+ dev: true
+
+ /eslint-plugin-promise@6.1.1(eslint@8.49.0):
+ resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ dependencies:
+ eslint: 8.49.0
+ dev: true
+
+ /eslint-plugin-standard@5.0.0(eslint@8.49.0):
+ resolution: {integrity: sha512-eSIXPc9wBM4BrniMzJRBm2uoVuXz2EPa+NXPk2+itrVt+r5SbKFERx/IgrK/HmfjddyKVz2f+j+7gBRvu19xLg==}
+ deprecated: 'standard 16.0.0 and eslint-config-standard 16.0.0 no longer require the eslint-plugin-standard package. You can remove it from your dependencies with ''npm rm eslint-plugin-standard''. More info here: https://github.com/standard/standard/issues/1316'
+ peerDependencies:
+ eslint: '>=5.0.0'
+ dependencies:
+ eslint: 8.49.0
+ dev: true
+
/eslint-plugin-vue@9.17.0(eslint@8.49.0):
resolution: {integrity: sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ==}
engines: {node: ^14.17.0 || >=16.0.0}
@@ -11305,6 +11737,14 @@ packages:
resolution: {integrity: sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==}
dev: true
+ /eslint-scope@5.1.1:
+ resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
+ engines: {node: '>=8.0.0'}
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 4.3.0
+ dev: true
+
/eslint-scope@7.2.0:
resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -11321,6 +11761,33 @@ packages:
estraverse: 5.3.0
dev: true
+ /eslint-utils@2.1.0:
+ resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==}
+ engines: {node: '>=6'}
+ dependencies:
+ eslint-visitor-keys: 1.3.0
+ dev: true
+
+ /eslint-utils@3.0.0(eslint@8.49.0):
+ resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
+ engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
+ peerDependencies:
+ eslint: '>=5'
+ dependencies:
+ eslint: 8.49.0
+ eslint-visitor-keys: 2.1.0
+ dev: true
+
+ /eslint-visitor-keys@1.3.0:
+ resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /eslint-visitor-keys@2.1.0:
+ resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==}
+ engines: {node: '>=10'}
+ dev: true
+
/eslint-visitor-keys@3.4.1:
resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -11415,6 +11882,11 @@ packages:
estraverse: 5.3.0
dev: true
+ /estraverse@4.3.0:
+ resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==}
+ engines: {node: '>=4.0'}
+ dev: true
+
/estraverse@5.3.0:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
@@ -11674,6 +12146,10 @@ packages:
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ /fast-diff@1.3.0:
+ resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
+ dev: true
+
/fast-fifo@1.3.0:
resolution: {integrity: sha512-IgfweLvEpwyA4WgiQe9Nx6VV2QkML2NkvZnk1oKnIzXgXdWxuhF7zw4DvLTPZJn6PIUneiAXPF24QmoEqHTjyw==}
@@ -11731,6 +12207,26 @@ packages:
strnum: 1.0.5
dev: false
+ /fastify-multer@2.0.3:
+ resolution: {integrity: sha512-QnFqrRgxmUwWHTgX9uyQSu0C/hmVCfcxopqjApZ4uaZD5W9MJ+nHUlW4+9q7Yd3BRxDIuHvgiM5mjrh6XG8cAA==}
+ engines: {node: '>=10.17.0'}
+ dependencies:
+ '@fastify/busboy': 1.1.0
+ append-field: 1.0.0
+ concat-stream: 2.0.0
+ fastify-plugin: 2.3.4
+ mkdirp: 1.0.4
+ on-finished: 2.4.1
+ type-is: 1.6.18
+ xtend: 4.0.2
+ dev: false
+
+ /fastify-plugin@2.3.4:
+ resolution: {integrity: sha512-I+Oaj6p9oiRozbam30sh39BiuiqBda7yK2nmSPVwDCfIBlKnT8YB3MY+pRQc2Fcd07bf6KPGklHJaQ2Qu81TYQ==}
+ dependencies:
+ semver: 7.5.4
+ dev: false
+
/fastify-plugin@4.5.0:
resolution: {integrity: sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg==}
dev: false
@@ -12183,6 +12679,7 @@ packages:
function-bind: 1.1.1
has: 1.0.3
has-symbols: 1.0.3
+ dev: true
/get-intrinsic@1.2.1:
resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
@@ -12191,7 +12688,6 @@ packages:
has: 1.0.3
has-proto: 1.0.1
has-symbols: 1.0.3
- dev: true
/get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
@@ -12542,13 +13038,12 @@ packages:
/has-property-descriptors@1.0.0:
resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
dependencies:
- get-intrinsic: 1.2.0
+ get-intrinsic: 1.2.1
dev: true
/has-proto@1.0.1:
resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
engines: {node: '>= 0.4'}
- dev: true
/has-symbols@1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
@@ -12887,7 +13382,7 @@ packages:
resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==}
engines: {node: '>= 0.4'}
dependencies:
- get-intrinsic: 1.2.0
+ get-intrinsic: 1.2.1
has: 1.0.3
side-channel: 1.0.4
dev: true
@@ -13035,12 +13530,6 @@ packages:
dependencies:
has: 1.0.3
- /is-core-module@2.12.1:
- resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==}
- dependencies:
- has: 1.0.3
- dev: true
-
/is-core-module@2.13.0:
resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==}
dependencies:
@@ -13288,7 +13777,7 @@ packages:
resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==}
dependencies:
call-bind: 1.0.2
- get-intrinsic: 1.2.0
+ get-intrinsic: 1.2.1
dev: true
/is-wsl@2.2.0:
@@ -13329,7 +13818,7 @@ packages:
engines: {node: '>=8'}
dependencies:
'@babel/core': 7.22.11
- '@babel/parser': 7.22.11
+ '@babel/parser': 7.22.16
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.0
semver: 6.3.1
@@ -13439,6 +13928,34 @@ packages:
- supports-color
dev: true
+ /jest-cli@29.7.0(@types/node@18.11.18):
+ resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ hasBin: true
+ peerDependencies:
+ node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
+ peerDependenciesMeta:
+ node-notifier:
+ optional: true
+ dependencies:
+ '@jest/core': 29.7.0
+ '@jest/test-result': 29.7.0
+ '@jest/types': 29.6.3
+ chalk: 4.1.2
+ create-jest: 29.7.0(@types/node@18.11.18)
+ exit: 0.1.2
+ import-local: 3.1.0
+ jest-config: 29.7.0(@types/node@18.11.18)
+ jest-util: 29.7.0
+ jest-validate: 29.7.0
+ yargs: 17.6.2
+ transitivePeerDependencies:
+ - '@types/node'
+ - babel-plugin-macros
+ - supports-color
+ - ts-node
+ dev: true
+
/jest-cli@29.7.0(@types/node@20.6.3):
resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -13467,6 +13984,46 @@ packages:
- ts-node
dev: true
+ /jest-config@29.7.0(@types/node@18.11.18):
+ resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ peerDependencies:
+ '@types/node': '*'
+ ts-node: '>=9.0.0'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ ts-node:
+ optional: true
+ dependencies:
+ '@babel/core': 7.22.11
+ '@jest/test-sequencer': 29.7.0
+ '@jest/types': 29.6.3
+ '@types/node': 18.11.18
+ babel-jest: 29.7.0(@babel/core@7.22.11)
+ chalk: 4.1.2
+ ci-info: 3.7.1
+ deepmerge: 4.2.2
+ glob: 7.2.3
+ graceful-fs: 4.2.11
+ jest-circus: 29.7.0
+ jest-environment-node: 29.7.0
+ jest-get-type: 29.6.3
+ jest-regex-util: 29.6.3
+ jest-resolve: 29.7.0
+ jest-runner: 29.7.0
+ jest-util: 29.7.0
+ jest-validate: 29.7.0
+ micromatch: 4.0.5
+ parse-json: 5.2.0
+ pretty-format: 29.7.0
+ slash: 3.0.0
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - babel-plugin-macros
+ - supports-color
+ dev: true
+
/jest-config@29.7.0(@types/node@20.6.3):
resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -13774,7 +14331,7 @@ packages:
'@babel/generator': 7.22.10
'@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.11)
'@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.22.11)
- '@babel/types': 7.22.11
+ '@babel/types': 7.22.17
'@jest/expect-utils': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
@@ -13849,6 +14406,27 @@ packages:
supports-color: 8.1.1
dev: true
+ /jest@29.7.0(@types/node@18.11.18):
+ resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ hasBin: true
+ peerDependencies:
+ node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
+ peerDependenciesMeta:
+ node-notifier:
+ optional: true
+ dependencies:
+ '@jest/core': 29.7.0
+ '@jest/types': 29.6.3
+ import-local: 3.1.0
+ jest-cli: 29.7.0(@types/node@18.11.18)
+ transitivePeerDependencies:
+ - '@types/node'
+ - babel-plugin-macros
+ - supports-color
+ - ts-node
+ dev: true
+
/jest@29.7.0(@types/node@20.6.3):
resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -14307,7 +14885,6 @@ packages:
/lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
- dev: false
/lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -14407,6 +14984,10 @@ packages:
resolution: {integrity: sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==}
engines: {node: 14 || >=16.14}
+ /lunr@2.3.9:
+ resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
+ dev: true
+
/luxon@3.3.0:
resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==}
engines: {node: '>=12'}
@@ -14467,6 +15048,10 @@ packages:
semver: 7.5.4
dev: true
+ /make-error@1.3.6:
+ resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
+ dev: true
+
/make-fetch-happen@11.1.1:
resolution: {integrity: sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -14523,6 +15108,12 @@ packages:
react: 18.2.0
dev: true
+ /marked@4.3.0:
+ resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==}
+ engines: {node: '>= 12'}
+ hasBin: true
+ dev: true
+
/matter-js@0.19.0:
resolution: {integrity: sha512-v2huwvQGOHTGOkMqtHd2hercCG3f6QAObTisPPHg8TZqq2lz7eIY/5i/5YUV8Ibf3mEioFEmwibcPUF2/fnKKQ==}
dev: false
@@ -14957,6 +15548,10 @@ packages:
/napi-build-utils@1.0.2:
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
+ /natural-compare-lite@1.4.0:
+ resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==}
+ dev: true
+
/natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true
@@ -15208,7 +15803,7 @@ packages:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies:
hosted-git-info: 2.8.9
- resolve: 1.22.1
+ resolve: 1.22.3
semver: 5.7.1
validate-npm-package-license: 3.0.4
dev: true
@@ -15316,23 +15911,24 @@ packages:
resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==}
dev: false
+ /object-assign-deep@0.4.0:
+ resolution: {integrity: sha512-54Uvn3s+4A/cMWx9tlRez1qtc7pN7pbQ+Yi7mjLjcBpWLlP+XbSHiHbQW6CElDiV4OvuzqnMrBdkgxI1mT8V/Q==}
+ engines: {node: '>=6'}
+ dev: false
+
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
- /object-inspect@1.12.2:
- resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
-
/object-inspect@1.12.3:
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
- dev: true
/object-is@1.1.5:
resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.0
dev: true
/object-keys@1.1.1:
@@ -15345,7 +15941,7 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.0
has-symbols: 1.0.3
object-keys: 1.1.1
dev: true
@@ -15614,12 +16210,18 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
dependencies:
- '@babel/code-frame': 7.22.5
+ '@babel/code-frame': 7.22.13
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
dev: true
+ /parse-link-header@2.0.0:
+ resolution: {integrity: sha512-xjU87V0VyHZybn2RrCX5TIFGxTVZE6zqqZWMPlIKiSKuWh/X5WZdt+w1Ki1nXB+8L/KtL+nZ4iq+sfI6MrhhMw==}
+ dependencies:
+ xtend: 4.0.2
+ dev: false
+
/parse-srcset@1.0.2:
resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
dev: false
@@ -16318,6 +16920,13 @@ packages:
engines: {node: '>= 0.8.0'}
dev: true
+ /prettier-linter-helpers@1.0.0:
+ resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ fast-diff: 1.3.0
+ dev: true
+
/prettier@2.8.8:
resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
engines: {node: '>=10.13.0'}
@@ -16468,6 +17077,10 @@ packages:
resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==}
dev: true
+ /proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+ dev: false
+
/ps-tree@1.2.0:
resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==}
engines: {node: '>= 0.10'}
@@ -17112,7 +17725,7 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.0
functions-have-names: 1.2.3
dev: true
@@ -17125,6 +17738,11 @@ packages:
functions-have-names: 1.2.3
dev: true
+ /regexpp@3.2.0:
+ resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==}
+ engines: {node: '>=8'}
+ dev: true
+
/regexpu-core@5.3.2:
resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==}
engines: {node: '>=4'}
@@ -17262,7 +17880,7 @@ packages:
resolution: {integrity: sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw==}
hasBin: true
dependencies:
- is-core-module: 2.12.1
+ is-core-module: 2.13.0
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
dev: true
@@ -17593,12 +18211,20 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ /shiki@0.12.1:
+ resolution: {integrity: sha512-aieaV1m349rZINEBkjxh2QbBvFFQOlgqYTNtCal82hHj4dDZ76oMlQIX+C7ryerBTDiga3e5NfH6smjdJ02BbQ==}
+ dependencies:
+ jsonc-parser: 3.2.0
+ vscode-oniguruma: 1.7.0
+ vscode-textmate: 8.0.0
+ dev: true
+
/side-channel@1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
call-bind: 1.0.2
- get-intrinsic: 1.2.0
- object-inspect: 1.12.2
+ get-intrinsic: 1.2.1
+ object-inspect: 1.12.3
/siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -18620,6 +19246,40 @@ packages:
engines: {node: '>=6.10'}
dev: true
+ /ts-jest@29.0.5(@babel/core@7.22.11)(jest@29.7.0)(typescript@4.9.4):
+ resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ hasBin: true
+ peerDependencies:
+ '@babel/core': '>=7.0.0-beta.0 <8'
+ '@jest/types': ^29.0.0
+ babel-jest: ^29.0.0
+ esbuild: '*'
+ jest: ^29.0.0
+ typescript: '>=4.3'
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ '@jest/types':
+ optional: true
+ babel-jest:
+ optional: true
+ esbuild:
+ optional: true
+ dependencies:
+ '@babel/core': 7.22.11
+ bs-logger: 0.2.6
+ fast-json-stable-stringify: 2.1.0
+ jest: 29.7.0(@types/node@18.11.18)
+ jest-util: 29.7.0
+ json5: 2.2.3
+ lodash.memoize: 4.1.2
+ make-error: 1.3.6
+ semver: 7.5.4
+ typescript: 4.9.4
+ yargs-parser: 21.1.1
+ dev: true
+
/ts-map@1.0.3:
resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==}
dev: true
@@ -18684,6 +19344,16 @@ packages:
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
+ /tsutils@3.21.0(typescript@4.9.4):
+ resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
+ engines: {node: '>= 6'}
+ peerDependencies:
+ typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
+ dependencies:
+ tslib: 1.14.1
+ typescript: 4.9.4
+ dev: true
+
/tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
dependencies:
@@ -18789,6 +19459,19 @@ packages:
/typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
+
+ /typedoc@0.23.24(typescript@4.9.4):
+ resolution: {integrity: sha512-bfmy8lNQh+WrPYcJbtjQ6JEEsVl/ce1ZIXyXhyW+a1vFrjO39t6J8sL/d6FfAGrJTc7McCXgk9AanYBSNvLdIA==}
+ engines: {node: '>= 14.14'}
+ hasBin: true
+ peerDependencies:
+ typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x
+ dependencies:
+ lunr: 2.3.9
+ marked: 4.3.0
+ minimatch: 5.1.2
+ shiki: 0.12.1
+ typescript: 4.9.4
dev: true
/typeorm@0.3.17(ioredis@5.3.2)(pg@8.11.3):
@@ -18870,6 +19553,11 @@ packages:
- supports-color
dev: false
+ /typescript@4.9.4:
+ resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
+ engines: {node: '>=4.2.0'}
+ hasBin: true
+
/typescript@5.0.4:
resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==}
engines: {node: '>=12.20'}
@@ -19318,6 +20006,14 @@ packages:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
+ /vscode-oniguruma@1.7.0:
+ resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
+ dev: true
+
+ /vscode-textmate@8.0.0:
+ resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
+ dev: true
+
/vue-component-type-helpers@1.8.13:
resolution: {integrity: sha512-zbCQviVRexZ7NF2kizQq5LicG5QGXPHPALKE3t59f5q2FwaG9GKtdhhIV4rw4LDUm9RkvGAP8TSXlXcBWY8rFQ==}
dev: true
@@ -19663,6 +20359,19 @@ packages:
async-limiter: 1.0.1
dev: true
+ /ws@8.12.0:
+ resolution: {integrity: sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+ dev: false
+
/ws@8.14.2(bufferutil@4.0.7)(utf-8-validate@6.0.3):
resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==}
engines: {node: '>=10.0.0'}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ead1764a56..ef2bb67209 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -3,3 +3,4 @@ packages:
- 'packages/frontend'
- 'packages/sw'
- 'packages/misskey-js'
+ - 'packages/megalodon'
diff --git a/scripts/clean-all.js b/scripts/clean-all.js
index 4735eed760..e4f5acae0d 100644
--- a/scripts/clean-all.js
+++ b/scripts/clean-all.js
@@ -16,6 +16,8 @@ const fs = require('fs');
fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/sw/node_modules', { recursive: true, force: true });
+ fs.rmSync(__dirname + '/../packages/megalodon/lib', { recursive: true, force: true });
+
fs.rmSync(__dirname + '/../built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../node_modules', { recursive: true, force: true });
diff --git a/scripts/clean.js b/scripts/clean.js
index 812553e17b..df1d33888d 100644
--- a/scripts/clean.js
+++ b/scripts/clean.js
@@ -10,4 +10,5 @@ const fs = require('fs');
fs.rmSync(__dirname + '/../packages/frontend/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../built', { recursive: true, force: true });
+ fs.rmSync(__dirname + '/../packages/megalodon/lib', { recursive: true, force: true });
})();
diff --git a/scripts/dev.mjs b/scripts/dev.mjs
index cf27517a3d..3fccfbc936 100644
--- a/scripts/dev.mjs
+++ b/scripts/dev.mjs
@@ -35,6 +35,12 @@ await execa('pnpm', ['--filter', 'misskey-js', 'build'], {
stderr: process.stderr,
});
+await execa("pnpm", ['--filter', 'megalodon', 'build'], {
+ cwd: _dirname + '/../',
+ stdout: process.stdout,
+ stderr: process.stderr,
+});
+
execa('pnpm', ['build-assets', '--watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,