diff --git a/.gitignore b/.gitignore
index a66e527db0590761d151a1ed0bf4e2a747ca5d9d..11e69b26211f93a7b7862f273d154d53b9fea318 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 a417355cfaac262847f2e4deb0e012766a222477..76e99a9dd0732654733c50005f4043f6ecd90c28 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 3d3fc870096be52f3f32723a8cd5426f8c57ffc0..1c2ffcfb6d9ff45d7f772eb46cfd634f8c420742 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 fa81380f01048e41ce0813e4172d25b50533b66d..fc6f0196022a0ebd3ccd64905ce45b1cd32c1578 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 0e4a5ece3ec91fd00c686112f6e51689a32934bf..a1189e219846281cb0abf1b62c3939ce81dae0da 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 0000000000000000000000000000000000000000..b79489d18df8caa8295b4c118393397c5b529056
--- /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 0000000000000000000000000000000000000000..94b70230d833e5b7b988b9f57b75457e08ad12fc
--- /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 0000000000000000000000000000000000000000..a37742a06807ca6564dec73c49642b53c6e77e66
--- /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 0000000000000000000000000000000000000000..3403b94b472d04e74a93614f325375a26c5d81b6
--- /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 0000000000000000000000000000000000000000..f19fe38a2b5c2986ca934209df3c910ce05921e3
--- /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 0000000000000000000000000000000000000000..f8e4729b8e6339b85bd8192985ce07af6993cc7e
--- /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 0000000000000000000000000000000000000000..93d669fa7d1611e6e24534191d4cf04d15a3d5e7
--- /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 0000000000000000000000000000000000000000..45bce13e2106472afc4c8950dc953ca9fe307ec2
--- /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 0000000000000000000000000000000000000000..06a85eb98e290b3627b63ddb829f61a386a9e14b
--- /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 0000000000000000000000000000000000000000..6bc0b6d80ed08de2bcb98c73be0474d208b04a5a
--- /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 0000000000000000000000000000000000000000..7c7983163460c160f0958d4fa34b5ef2c767cfad
--- /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 0000000000000000000000000000000000000000..9b98b1277278f5f2595468639a138e5f14a412fc
--- /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 0000000000000000000000000000000000000000..9cc17acc5c61ab17fdd3d789e353279f742a2123
--- /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 0000000000000000000000000000000000000000..082c79eddbca7cc05bee6a4ada677eba602fea74
--- /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 0000000000000000000000000000000000000000..356d99aee42846ccb4426a69b59e76729aa3df03
--- /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 0000000000000000000000000000000000000000..a794a7c5a80290b29fee26442275e991921663b6
--- /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 0000000000000000000000000000000000000000..2bdc1966615659ee8befb32182e19077a1919b0d
--- /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 0000000000000000000000000000000000000000..10c32ab0bdcb053bd3628801ebf4f54f3f9dace0
--- /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 0000000000000000000000000000000000000000..fc9f8c69ccd956d3f8478a2bd093313e31d36c7a
--- /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 0000000000000000000000000000000000000000..de4b6b2b72fc3bd396edabb1c6e3129286770fb2
--- /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 0000000000000000000000000000000000000000..55b7305cc33811b2ea091d3c4a40875a61fa1f62
--- /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 0000000000000000000000000000000000000000..4676357d6992418f4781b257ca5eab3930e74137
--- /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 0000000000000000000000000000000000000000..3b42e6f41230e9567293cab9479a4713e06f6937
--- /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 0000000000000000000000000000000000000000..9c0f572db417954a74b43724dacf0bdbef273caf
--- /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 0000000000000000000000000000000000000000..97e75286b229bb2db1d91709fd4be6ccdddccd5e
--- /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 0000000000000000000000000000000000000000..7ee99282ca57e1217dd7ddf1fba48dadc12aa2a4
--- /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 0000000000000000000000000000000000000000..4fe36a65539e52b8127b238e0bfa903820f28eb8
--- /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 0000000000000000000000000000000000000000..68eff3347ef701e05f5230e09ef58cac7a97b274
--- /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 0000000000000000000000000000000000000000..2539d68b20c47769a453e49173f0f577ce76a840
--- /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 0000000000000000000000000000000000000000..e818a8607b7a8206aa8c5cbe9d2711e0ca04dc2a
--- /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 0000000000000000000000000000000000000000..7994dc568ebd086a809de6780c37e7585912a877
--- /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 0000000000000000000000000000000000000000..ad1146a242c7018a2677363b8bab3f0582a853de
--- /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 0000000000000000000000000000000000000000..4edbec6a7d03d7b09c0dc28bed028ddc1c556017
--- /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 0000000000000000000000000000000000000000..91802d5c884956691cd2aeafd6f668c69e8ced01
--- /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 0000000000000000000000000000000000000000..6862a5fabe64926c875a39a7ac8897b4553e0c9b
--- /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 0000000000000000000000000000000000000000..4448e5335050a845c1a5c547f4f132ce900e7e83
--- /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 0000000000000000000000000000000000000000..78dfb8ed261dfb935e6c5476115529c420f66035
--- /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 0000000000000000000000000000000000000000..913b02fda7dda171fe4a14b0e1e7c3157325d2aa
--- /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 0000000000000000000000000000000000000000..6471df039af70d6720e16397cdd125e5af06e996
--- /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 0000000000000000000000000000000000000000..f27f728b545f55c187a1334e86019ea082e558ef
--- /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 0000000000000000000000000000000000000000..4040b4ff909e0469e84d6704ec193f2b1d20433f
--- /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 0000000000000000000000000000000000000000..18908c01c15ea3c9e185515f228e48cfc6812fa5
--- /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 0000000000000000000000000000000000000000..ccc88aece6680db8316e5ca7a9d3923881a993f5
--- /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 0000000000000000000000000000000000000000..1583edafb1254eaeae79d2e4873051d3aed1665e
--- /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 0000000000000000000000000000000000000000..1ee9ed67c9770c46598750ea95356fd3f4a4fdb8
--- /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 0000000000000000000000000000000000000000..b73d2b359b89de092297745c611121a35bd50b0e
--- /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 0000000000000000000000000000000000000000..4c83cb15f2eafe32f7b8ec43310937bc8dd890e9
--- /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 0000000000000000000000000000000000000000..758d3a46ad19b587dc4db2c2b081a1b72975b2d1
--- /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 0000000000000000000000000000000000000000..33a5790f67a42d0a1d8cafd314977ab56d9b2ebf
--- /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 0000000000000000000000000000000000000000..edfaa4f3cb3e178ed99aeebe04cc9e77552b74ae
--- /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 0000000000000000000000000000000000000000..a0b01030d8544fe1e972d9ee8c4c103e865870a4
--- /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 0000000000000000000000000000000000000000..94ace2f1840f5b8bfb78a46db0e81e6c7a3b43e9
--- /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 0000000000000000000000000000000000000000..7594ba7efc7956eb9ba89f209a4b9baa516b61bb
--- /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 0000000000000000000000000000000000000000..5924060d811cf3f3c6bacfa03133eb392a241497
--- /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 0000000000000000000000000000000000000000..3e56790a7bbe56150e2180c654a47bcaf80007fe
--- /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 0000000000000000000000000000000000000000..235f7063fbe748cb2ace16af39dcc69d737e0d6a
--- /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 0000000000000000000000000000000000000000..d320760e919c61fd30c2826690ad0fa1aaa056a3
--- /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 0000000000000000000000000000000000000000..ba948f2e7389b892f85a80082183c86f31cad109
--- /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 0000000000000000000000000000000000000000..8bbb2d7c42d50dc40d4026f10efdc78afb99ff11
--- /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 0000000000000000000000000000000000000000..e823dde1be9ace8733742d11e875e8723a48a26a
--- /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 0000000000000000000000000000000000000000..60bd0e0abcf94666eca0f9a81a55036ec1c1e5db
--- /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 0000000000000000000000000000000000000000..34ae82551973dae2f9aaa7b51ace447b2986fed2
--- /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 0000000000000000000000000000000000000000..6cbc8f1c396e377880e8141ebad75428e78962b4
--- /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 0000000000000000000000000000000000000000..3ec4d6675b51a18d6f7bf317ee71eac0ad9e9a9f
--- /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 0000000000000000000000000000000000000000..60706592a43d7e143bb74f14b67665aa6e026cfb
--- /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 0000000000000000000000000000000000000000..97827fe8fdffc44e0ea289c7c58b52478e6e07f5
--- /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 0000000000000000000000000000000000000000..7975b3d3158fece9b0f7297af58f62dd92d9e6c9
--- /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 0000000000000000000000000000000000000000..64a0a507850712cd7710a91cefb9b6286b6acd3d
--- /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 0000000000000000000000000000000000000000..7ecb911537cff09f92d7d64fc5376f842c65edca
--- /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 0000000000000000000000000000000000000000..9f6bfa40d2f7bc2e4dabb794f81a1f735d8200e7
--- /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 0000000000000000000000000000000000000000..b35a25bfb539282bbace2ff2cb02db083afa03d3
--- /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 0000000000000000000000000000000000000000..6db4a1b167a058b24275e1090fb69157170ad57c
--- /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 0000000000000000000000000000000000000000..572333ff0bfdf6d915bb49a9c40e9f78a005586b
--- /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 0000000000000000000000000000000000000000..62d60ce282671689a284689278aaca7ebd610de5
--- /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 0000000000000000000000000000000000000000..9832a0ad8a5ccd89131248037f89e8b659174621
--- /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 0000000000000000000000000000000000000000..96610f6e6d9e6b3e976103480671e0964c0ae835
--- /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 0000000000000000000000000000000000000000..0f5bd5f644bdf1495923b8285656679f77c1942b
--- /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 0000000000000000000000000000000000000000..272e65ffa405c686b0475cacb9b013cdde30aeb9
--- /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 0000000000000000000000000000000000000000..921af655366f6ef9d272c241c7c67357edc61e98
--- /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 0000000000000000000000000000000000000000..72a80f9d96d8cffc79f701cde14c8c5d67debf9d
--- /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 0000000000000000000000000000000000000000..eb7c2d23d8d9e27a67af987948a4e5933ce4c575
--- /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 0000000000000000000000000000000000000000..0cbfc2bfeb42e23e2a00fc55deb6b1c81ef66f8e
--- /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 0000000000000000000000000000000000000000..84cd23e40d0b171b2085645d6c64f5caa46a0c60
--- /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 0000000000000000000000000000000000000000..f0df721f0a327f1e7cda6250481b229016fdb6b4
--- /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 0000000000000000000000000000000000000000..2ddf2ac2e692eb493fb2438418ffdfa5b67bd9fb
--- /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 0000000000000000000000000000000000000000..fadbcf084e41bad7c1388e55d75a47763f62daa9
--- /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 0000000000000000000000000000000000000000..13fd8ab574acd580772c1f3a4d435407b1731e6a
--- /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 0000000000000000000000000000000000000000..8964535509b393fc1441c1df6168f65eea46b6a1
--- /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 0000000000000000000000000000000000000000..0ec1288428109f9b49147fbbdd865b94090170f5
--- /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 0000000000000000000000000000000000000000..7cf33b983d7f49ae19537935e7ed7782e90900f1
--- /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 0000000000000000000000000000000000000000..5174a647c66c8df97f46fbf931f744bc8560b63f
--- /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 0000000000000000000000000000000000000000..5a9bfbde9a9cf6c486f8df8f6437d1b5abdd43d6
--- /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 f33ab1c33cc32d5963c856d7a89b6a97a13a76ee..0f9254216adeeec2ce983e8b7f68cc6c5446b87c 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 902102d84e7df53480a17c11f83061cc8671649d..42faf9301c6239181975c50a955f33f9ec9d722f 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,7 +11526,7 @@ packages:
       - 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):
+  /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:
@@ -11240,7 +11547,7 @@ packages:
       eslint-import-resolver-webpack:
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 6.7.2(eslint@8.49.0)(typescript@5.2.2)
+      '@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
@@ -11248,28 +11555,103 @@ packages:
       - 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==}
+  /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'}
     peerDependencies:
       '@typescript-eslint/parser': '*'
-      eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
+      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': 6.7.2(eslint@8.49.0)(typescript@5.2.2)
-      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@6.7.2)(eslint-import-resolver-node@0.3.7)(eslint@8.49.0)
-      has: 1.0.3
-      is-core-module: 2.13.0
+    transitivePeerDependencies:
+      - 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'}
+    peerDependencies:
+      '@typescript-eslint/parser': '*'
+      eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
+    peerDependenciesMeta:
+      '@typescript-eslint/parser':
+        optional: true
+    dependencies:
+      '@typescript-eslint/parser': 6.7.2(eslint@8.49.0)(typescript@5.2.2)
+      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@6.7.2)(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
@@ -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 ead1764a56ca779d92ce55257ea2e5fdbe0f5c49..ef2bb6720985e820bba178f1a6c8a4faf0b221f2 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 4735eed760dbd105b4eb701f89ff8dc600fb40a0..e4f5acae0df041d83e19dca0dba65578e26eaa6d 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 812553e17b3632a60ae4174075a712383d452d37..df1d33888d821207917e1321800b5a9ad433ab2a 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 cf27517a3d558e697ac257434f8239082abad1c4..3fccfbc936519facb4a5e618e71a7d2fe0791f96 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,