diff --git a/.gitignore b/.gitignore index 0a1a09c90d..3c55ec0d78 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,9 @@ packages/backend/assets/LICENSE !/packages/backend/src/db !/packages/backend/src/server/api/endpoints/drive/files +packages/megalodon/lib +packages/megalodon/.idea + # blender backups *.blend1 *.blend2 diff --git a/Dockerfile b/Dockerfile index 7ba32ba832..c7d9a95d86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ COPY packages/backend/package.json packages/backend/package.json COPY packages/client/package.json packages/client/package.json COPY packages/sw/package.json packages/sw/package.json COPY packages/firefish-js/package.json packages/firefish-js/package.json +COPY packages/megalodon/package.json packages/megalodon/package.json COPY packages/backend/native-utils/package.json packages/backend/native-utils/package.json COPY packages/backend/native-utils/npm/linux-x64-musl/package.json packages/backend/native-utils/npm/linux-x64-musl/package.json COPY packages/backend/native-utils/npm/linux-arm64-musl/package.json packages/backend/native-utils/npm/linux-arm64-musl/package.json @@ -55,6 +56,8 @@ RUN apt-get update && apt-get install -y libvips-dev zip unzip tini ffmpeg COPY . ./ +COPY --from=build /firefish/packages/megalodon /firefish/packages/megalodon + # Copy node modules COPY --from=build /firefish/node_modules /firefish/node_modules COPY --from=build /firefish/packages/backend/node_modules /firefish/packages/backend/node_modules diff --git a/packages/README.md b/packages/README.md index 75e38a4940..ea04817f06 100644 --- a/packages/README.md +++ b/packages/README.md @@ -7,3 +7,4 @@ This directory contains all of the packages Firefish uses. - `client`: Web interface written in Vue3 and TypeScript - `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript - `firefish-js`: TypeScript SDK for both backend and client, also published on [NPM](https://www.npmjs.com/package/firefish-js) for public use +- `megalodon`: TypeScript library used for partial Mastodon API compatibility diff --git a/packages/backend/package.json b/packages/backend/package.json index 2a84da576f..110b8e14ef 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -87,7 +87,7 @@ "koa-send": "5.0.1", "koa-slow": "2.1.0", "koa-views": "7.0.2", - "megalodon": "8.1.1", + "megalodon": "workspace:*", "meilisearch": "0.34.1", "mfm-js": "0.23.3", "mime-types": "2.1.35", diff --git a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts index 3f6e671bd6..44485ac429 100644 --- a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts +++ b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts @@ -24,11 +24,7 @@ export function getClient( const accessTokenArr = authorization?.split(" ") ?? [null]; const accessToken = accessTokenArr[accessTokenArr.length - 1]; const generator = (megalodon as any).default; - const client = generator( - "firefish", - BASE_URL, - accessToken, - ) as MegalodonInterface; + const client = generator(BASE_URL, accessToken) as MegalodonInterface; return client; } diff --git a/packages/backend/src/server/api/mastodon/endpoints/auth.ts b/packages/backend/src/server/api/mastodon/endpoints/auth.ts index 3d6eb8c4d6..b55cb6388c 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/auth.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/auth.ts @@ -68,7 +68,7 @@ export function apiAuthMastodon(router: Router): void { website: body.website, redirect_uri: red, client_id: Buffer.from(appData.url || "").toString("base64"), - client_secret: appData.client_secret, + client_secret: appData.clientSecret, }; console.log(returns); ctx.body = returns; diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts index 5bd24ca89b..8a48175579 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/search.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts @@ -1,7 +1,8 @@ +import megalodon, { MegalodonInterface } from "megalodon"; import Router from "@koa/router"; import { getClient } from "../ApiMastodonCompatibleService.js"; import axios from "axios"; -import Converter from "megalodon"; +import { Converter } from "megalodon"; import { convertTimelinesArgsId, limitToInt } from "./timeline.js"; import { convertAccount, convertStatus } from "../converters.js"; diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index 2c35843dd3..bc95d77769 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -380,7 +380,7 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.createEmojiReaction( + const data = await client.reactStatus( convertId(ctx.params.id, IdType.FirefishId), ctx.params.name, ); @@ -400,7 +400,7 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.deleteEmojiReaction( + const data = await client.unreactStatus( convertId(ctx.params.id, IdType.FirefishId), ctx.params.name, ); diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index 5a47f2cba7..e483683151 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -25,7 +25,7 @@ import { readNotification } from "../common/read-notification.js"; import channels from "./channels/index.js"; import type Channel from "./channel.js"; import type { StreamEventEmitter, StreamMessages } from "./types.js"; -import Converter from "megalodon"; +import { Converter } from "megalodon"; import { getClient } from "../mastodon/ApiMastodonCompatibleService.js"; /** diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index b967754d46..f26853eb8f 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -163,10 +163,10 @@ mastoRouter.post("/oauth/token", async (ctx) => { ctx.body = ret; return; } - let client_id: Array | string | null = body.client_id; + let client_id: any = body.client_id; const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const generator = (megalodon as any).default; - const client = generator("firefish", BASE_URL, null) as MegalodonInterface; + const client = generator(BASE_URL, null) as MegalodonInterface; let token = null; if (body.code) { //m = body.code.match(/^([a-zA-Z0-9]{8})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{12})/); @@ -190,7 +190,7 @@ mastoRouter.post("/oauth/token", async (ctx) => { token ? token : "", ); const ret = { - access_token: atData.access_token, + access_token: atData.accessToken, token_type: "Bearer", scope: body.scope || "read write follow push", created_at: Math.floor(new Date().getTime() / 1000), diff --git a/packages/megalodon/package.json b/packages/megalodon/package.json new file mode 100644 index 0000000000..3403b94b47 --- /dev/null +++ b/packages/megalodon/package.json @@ -0,0 +1,83 @@ +{ + "name": "megalodon", + "private": true, + "main": "./lib/src/index.js", + "typings": "./lib/src/index.d.ts", + "scripts": { + "build": "tsc -p ./", + "build:debug": "pnpm run build", + "lint": "pnpm biome check **/*.ts --apply", + "format": "pnpm biome format --write src/**/*.ts", + "doc": "typedoc --out ../docs ./src", + "test": "NODE_ENV=test jest -u --maxWorkers=3" + }, + "jest": { + "moduleFileExtensions": [ + "ts", + "js" + ], + "moduleNameMapper": { + "^@/(.+)": "/src/$1", + "^~/(.+)": "/$1" + }, + "testMatch": [ + "**/test/**/*.spec.ts" + ], + "preset": "ts-jest/presets/default", + "transform": { + "^.+\\.(ts|tsx)$": "ts-jest" + }, + "globals": { + "ts-jest": { + "tsconfig": "tsconfig.json" + } + }, + "testEnvironment": "node" + }, + "dependencies": { + "@types/oauth": "^0.9.0", + "@types/ws": "^8.5.4", + "axios": "1.2.2", + "dayjs": "^1.11.7", + "form-data": "^4.0.0", + "https-proxy-agent": "^5.0.1", + "oauth": "^0.10.0", + "object-assign-deep": "^0.4.0", + "parse-link-header": "^2.0.0", + "socks-proxy-agent": "^7.0.0", + "typescript": "4.9.4", + "uuid": "^9.0.0", + "ws": "8.12.0", + "async-lock": "1.4.0" + }, + "devDependencies": { + "@types/core-js": "^2.5.0", + "@types/form-data": "^2.5.0", + "@types/jest": "^29.4.0", + "@types/object-assign-deep": "^0.4.0", + "@types/parse-link-header": "^2.0.0", + "@types/uuid": "^9.0.0", + "@types/node": "18.11.18", + "@typescript-eslint/eslint-plugin": "^5.49.0", + "@typescript-eslint/parser": "^5.49.0", + "@types/async-lock": "1.4.0", + "eslint": "^8.32.0", + "eslint-config-prettier": "^8.6.0", + "eslint-config-standard": "^16.0.3", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-node": "^11.0.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-standard": "^5.0.0", + "jest": "^29.4.0", + "jest-worker": "^29.4.0", + "lodash": "^4.17.14", + "prettier": "^2.8.3", + "ts-jest": "^29.0.5", + "typedoc": "^0.23.24" + }, + "directories": { + "lib": "lib", + "test": "test" + } +} diff --git a/packages/megalodon/src/axios.d.ts b/packages/megalodon/src/axios.d.ts new file mode 100644 index 0000000000..f19fe38a2b --- /dev/null +++ b/packages/megalodon/src/axios.d.ts @@ -0,0 +1 @@ +declare module "axios/lib/adapters/http"; diff --git a/packages/megalodon/src/cancel.ts b/packages/megalodon/src/cancel.ts new file mode 100644 index 0000000000..f8e4729b8e --- /dev/null +++ b/packages/megalodon/src/cancel.ts @@ -0,0 +1,13 @@ +export class RequestCanceledError extends Error { + public isCancel: boolean; + + constructor(msg: string) { + super(msg); + this.isCancel = true; + Object.setPrototypeOf(this, RequestCanceledError); + } +} + +export const isCancel = (value: any): boolean => { + return value && value.isCancel; +}; diff --git a/packages/megalodon/src/converter.ts b/packages/megalodon/src/converter.ts new file mode 100644 index 0000000000..93d669fa7d --- /dev/null +++ b/packages/megalodon/src/converter.ts @@ -0,0 +1,3 @@ +import MisskeyAPI from "./misskey/api_client"; + +export default MisskeyAPI.Converter; diff --git a/packages/megalodon/src/default.ts b/packages/megalodon/src/default.ts new file mode 100644 index 0000000000..45bce13e21 --- /dev/null +++ b/packages/megalodon/src/default.ts @@ -0,0 +1,3 @@ +export const NO_REDIRECT = "urn:ietf:wg:oauth:2.0:oob"; +export const DEFAULT_SCOPE = ["read", "write", "follow"]; +export const DEFAULT_UA = "megalodon"; diff --git a/packages/megalodon/src/entities/account.ts b/packages/megalodon/src/entities/account.ts new file mode 100644 index 0000000000..06a85eb98e --- /dev/null +++ b/packages/megalodon/src/entities/account.ts @@ -0,0 +1,27 @@ +/// +/// +/// +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; + moved: Account | null; + fields: Array; + bot: boolean | null; + source?: Source; + }; +} diff --git a/packages/megalodon/src/entities/activity.ts b/packages/megalodon/src/entities/activity.ts new file mode 100644 index 0000000000..6bc0b6d80e --- /dev/null +++ b/packages/megalodon/src/entities/activity.ts @@ -0,0 +1,8 @@ +namespace Entity { + export type Activity = { + week: string; + statuses: string; + logins: string; + registrations: string; + }; +} diff --git a/packages/megalodon/src/entities/announcement.ts b/packages/megalodon/src/entities/announcement.ts new file mode 100644 index 0000000000..7c79831634 --- /dev/null +++ b/packages/megalodon/src/entities/announcement.ts @@ -0,0 +1,34 @@ +/// +/// +/// + +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; + statuses: Array; + tags: Array; + emojis: Array; + reactions: Array; + }; + + export type AnnouncementAccount = { + id: string; + username: string; + url: string; + acct: string; + }; + + export type AnnouncementStatus = { + id: string; + url: string; + }; +} diff --git a/packages/megalodon/src/entities/application.ts b/packages/megalodon/src/entities/application.ts new file mode 100644 index 0000000000..9b98b12772 --- /dev/null +++ b/packages/megalodon/src/entities/application.ts @@ -0,0 +1,7 @@ +namespace Entity { + export type Application = { + name: string; + website?: string | null; + vapid_key?: string | null; + }; +} diff --git a/packages/megalodon/src/entities/async_attachment.ts b/packages/megalodon/src/entities/async_attachment.ts new file mode 100644 index 0000000000..9cc17acc5c --- /dev/null +++ b/packages/megalodon/src/entities/async_attachment.ts @@ -0,0 +1,14 @@ +/// +namespace Entity { + export type AsyncAttachment = { + id: string; + type: "unknown" | "image" | "gifv" | "video" | "audio"; + url: string | null; + remote_url: string | null; + preview_url: string; + text_url: string | null; + meta: Meta | null; + description: string | null; + blurhash: string | null; + }; +} diff --git a/packages/megalodon/src/entities/attachment.ts b/packages/megalodon/src/entities/attachment.ts new file mode 100644 index 0000000000..082c79eddb --- /dev/null +++ b/packages/megalodon/src/entities/attachment.ts @@ -0,0 +1,49 @@ +namespace Entity { + export type Sub = { + // For Image, Gifv, and Video + width?: number; + height?: number; + size?: string; + aspect?: number; + + // For Gifv and Video + frame_rate?: string; + + // For Audio, Gifv, and Video + duration?: number; + bitrate?: number; + }; + + export type Focus = { + x: number; + y: number; + }; + + export type Meta = { + original?: Sub; + small?: Sub; + focus?: Focus; + length?: string; + duration?: number; + fps?: number; + size?: string; + width?: number; + height?: number; + aspect?: number; + audio_encode?: string; + audio_bitrate?: string; + audio_channel?: string; + }; + + export type Attachment = { + id: string; + type: "unknown" | "image" | "gifv" | "video" | "audio"; + url: string; + remote_url: string | null; + preview_url: string | null; + text_url: string | null; + meta: Meta | null; + description: string | null; + blurhash: string | null; + }; +} diff --git a/packages/megalodon/src/entities/card.ts b/packages/megalodon/src/entities/card.ts new file mode 100644 index 0000000000..356d99aee4 --- /dev/null +++ b/packages/megalodon/src/entities/card.ts @@ -0,0 +1,16 @@ +namespace Entity { + export type Card = { + url: string; + title: string; + description: string; + type: "link" | "photo" | "video" | "rich"; + image?: string; + author_name?: string; + author_url?: string; + provider_name?: string; + provider_url?: string; + html?: string; + width?: number; + height?: number; + }; +} diff --git a/packages/megalodon/src/entities/context.ts b/packages/megalodon/src/entities/context.ts new file mode 100644 index 0000000000..a794a7c5a8 --- /dev/null +++ b/packages/megalodon/src/entities/context.ts @@ -0,0 +1,8 @@ +/// + +namespace Entity { + export type Context = { + ancestors: Array; + descendants: Array; + }; +} diff --git a/packages/megalodon/src/entities/conversation.ts b/packages/megalodon/src/entities/conversation.ts new file mode 100644 index 0000000000..2bdc196661 --- /dev/null +++ b/packages/megalodon/src/entities/conversation.ts @@ -0,0 +1,11 @@ +/// +/// + +namespace Entity { + export type Conversation = { + id: string; + accounts: Array; + last_status: Status | null; + unread: boolean; + }; +} diff --git a/packages/megalodon/src/entities/emoji.ts b/packages/megalodon/src/entities/emoji.ts new file mode 100644 index 0000000000..10c32ab0bd --- /dev/null +++ b/packages/megalodon/src/entities/emoji.ts @@ -0,0 +1,9 @@ +namespace Entity { + export type Emoji = { + shortcode: string; + static_url: string; + url: string; + visible_in_picker: boolean; + category: string; + }; +} diff --git a/packages/megalodon/src/entities/featured_tag.ts b/packages/megalodon/src/entities/featured_tag.ts new file mode 100644 index 0000000000..fc9f8c69cc --- /dev/null +++ b/packages/megalodon/src/entities/featured_tag.ts @@ -0,0 +1,8 @@ +namespace Entity { + export type FeaturedTag = { + id: string; + name: string; + statuses_count: number; + last_status_at: string; + }; +} diff --git a/packages/megalodon/src/entities/field.ts b/packages/megalodon/src/entities/field.ts new file mode 100644 index 0000000000..de4b6b2b72 --- /dev/null +++ b/packages/megalodon/src/entities/field.ts @@ -0,0 +1,7 @@ +namespace Entity { + export type Field = { + name: string; + value: string; + verified_at: string | null; + }; +} diff --git a/packages/megalodon/src/entities/filter.ts b/packages/megalodon/src/entities/filter.ts new file mode 100644 index 0000000000..55b7305cc3 --- /dev/null +++ b/packages/megalodon/src/entities/filter.ts @@ -0,0 +1,12 @@ +namespace Entity { + export type Filter = { + id: string; + phrase: string; + context: Array; + expires_at: string | null; + irreversible: boolean; + whole_word: boolean; + }; + + export type FilterContext = string; +} diff --git a/packages/megalodon/src/entities/history.ts b/packages/megalodon/src/entities/history.ts new file mode 100644 index 0000000000..4676357d69 --- /dev/null +++ b/packages/megalodon/src/entities/history.ts @@ -0,0 +1,7 @@ +namespace Entity { + export type History = { + day: string; + uses: number; + accounts: number; + }; +} diff --git a/packages/megalodon/src/entities/identity_proof.ts b/packages/megalodon/src/entities/identity_proof.ts new file mode 100644 index 0000000000..3b42e6f412 --- /dev/null +++ b/packages/megalodon/src/entities/identity_proof.ts @@ -0,0 +1,9 @@ +namespace Entity { + export type IdentityProof = { + provider: string; + provider_username: string; + updated_at: string; + proof_url: string; + profile_url: string; + }; +} diff --git a/packages/megalodon/src/entities/instance.ts b/packages/megalodon/src/entities/instance.ts new file mode 100644 index 0000000000..9c0f572db4 --- /dev/null +++ b/packages/megalodon/src/entities/instance.ts @@ -0,0 +1,41 @@ +/// +/// +/// + +namespace Entity { + export type Instance = { + uri: string; + title: string; + description: string; + email: string; + version: string; + thumbnail: string | null; + urls: URLs; + stats: Stats; + languages: Array; + 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; + image_size_limit: number; + image_matrix_limit: number; + video_size_limit: number; + video_frame_limit: number; + video_matrix_limit: number; + }; + polls: { + max_options: number; + max_characters_per_option: number; + min_expiration: number; + max_expiration: number; + }; + }; + }; +} diff --git a/packages/megalodon/src/entities/list.ts b/packages/megalodon/src/entities/list.ts new file mode 100644 index 0000000000..97e75286b2 --- /dev/null +++ b/packages/megalodon/src/entities/list.ts @@ -0,0 +1,6 @@ +namespace Entity { + export type List = { + id: string; + title: string; + }; +} diff --git a/packages/megalodon/src/entities/marker.ts b/packages/megalodon/src/entities/marker.ts new file mode 100644 index 0000000000..7ee99282ca --- /dev/null +++ b/packages/megalodon/src/entities/marker.ts @@ -0,0 +1,15 @@ +namespace Entity { + export type Marker = { + home?: { + last_read_id: string; + version: number; + updated_at: string; + }; + notifications?: { + last_read_id: string; + version: number; + updated_at: string; + unread_count?: number; + }; + }; +} diff --git a/packages/megalodon/src/entities/mention.ts b/packages/megalodon/src/entities/mention.ts new file mode 100644 index 0000000000..4fe36a6553 --- /dev/null +++ b/packages/megalodon/src/entities/mention.ts @@ -0,0 +1,8 @@ +namespace Entity { + export type Mention = { + id: string; + username: string; + url: string; + acct: string; + }; +} diff --git a/packages/megalodon/src/entities/notification.ts b/packages/megalodon/src/entities/notification.ts new file mode 100644 index 0000000000..68eff3347e --- /dev/null +++ b/packages/megalodon/src/entities/notification.ts @@ -0,0 +1,15 @@ +/// +/// + +namespace Entity { + export type Notification = { + account: Account; + created_at: string; + id: string; + status?: Status; + reaction?: Reaction; + type: NotificationType; + }; + + export type NotificationType = string; +} diff --git a/packages/megalodon/src/entities/poll.ts b/packages/megalodon/src/entities/poll.ts new file mode 100644 index 0000000000..2539d68b20 --- /dev/null +++ b/packages/megalodon/src/entities/poll.ts @@ -0,0 +1,14 @@ +/// + +namespace Entity { + export type Poll = { + id: string; + expires_at: string | null; + expired: boolean; + multiple: boolean; + votes_count: number; + options: Array; + voted: boolean; + own_votes: Array; + }; +} diff --git a/packages/megalodon/src/entities/poll_option.ts b/packages/megalodon/src/entities/poll_option.ts new file mode 100644 index 0000000000..e818a8607b --- /dev/null +++ b/packages/megalodon/src/entities/poll_option.ts @@ -0,0 +1,6 @@ +namespace Entity { + export type PollOption = { + title: string; + votes_count: number | null; + }; +} diff --git a/packages/megalodon/src/entities/preferences.ts b/packages/megalodon/src/entities/preferences.ts new file mode 100644 index 0000000000..7994dc568e --- /dev/null +++ b/packages/megalodon/src/entities/preferences.ts @@ -0,0 +1,9 @@ +namespace Entity { + export type Preferences = { + "posting:default:visibility": "public" | "unlisted" | "private" | "direct"; + "posting:default:sensitive": boolean; + "posting:default:language": string | null; + "reading:expand:media": "default" | "show_all" | "hide_all"; + "reading:expand:spoilers": boolean; + }; +} diff --git a/packages/megalodon/src/entities/push_subscription.ts b/packages/megalodon/src/entities/push_subscription.ts new file mode 100644 index 0000000000..ad1146a242 --- /dev/null +++ b/packages/megalodon/src/entities/push_subscription.ts @@ -0,0 +1,16 @@ +namespace Entity { + export type Alerts = { + follow: boolean; + favourite: boolean; + mention: boolean; + reblog: boolean; + poll: boolean; + }; + + export type PushSubscription = { + id: string; + endpoint: string; + server_key: string; + alerts: Alerts; + }; +} diff --git a/packages/megalodon/src/entities/reaction.ts b/packages/megalodon/src/entities/reaction.ts new file mode 100644 index 0000000000..4edbec6a7d --- /dev/null +++ b/packages/megalodon/src/entities/reaction.ts @@ -0,0 +1,12 @@ +/// + +namespace Entity { + export type Reaction = { + count: number; + me: boolean; + name: string; + url?: string; + static_url?: string; + accounts?: Array; + }; +} diff --git a/packages/megalodon/src/entities/relationship.ts b/packages/megalodon/src/entities/relationship.ts new file mode 100644 index 0000000000..91802d5c88 --- /dev/null +++ b/packages/megalodon/src/entities/relationship.ts @@ -0,0 +1,17 @@ +namespace Entity { + export type Relationship = { + id: string; + following: boolean; + followed_by: boolean; + delivery_following?: boolean; + blocking: boolean; + blocked_by: boolean; + muting: boolean; + muting_notifications: boolean; + requested: boolean; + domain_blocking: boolean; + showing_reblogs: boolean; + endorsed: boolean; + notifying: boolean; + }; +} diff --git a/packages/megalodon/src/entities/report.ts b/packages/megalodon/src/entities/report.ts new file mode 100644 index 0000000000..6862a5fabe --- /dev/null +++ b/packages/megalodon/src/entities/report.ts @@ -0,0 +1,9 @@ +namespace Entity { + export type Report = { + id: string; + action_taken: string; + comment: string; + account_id: string; + status_ids: Array; + }; +} diff --git a/packages/megalodon/src/entities/results.ts b/packages/megalodon/src/entities/results.ts new file mode 100644 index 0000000000..4448e53350 --- /dev/null +++ b/packages/megalodon/src/entities/results.ts @@ -0,0 +1,11 @@ +/// +/// +/// + +namespace Entity { + export type Results = { + accounts: Array; + statuses: Array; + hashtags: Array; + }; +} diff --git a/packages/megalodon/src/entities/scheduled_status.ts b/packages/megalodon/src/entities/scheduled_status.ts new file mode 100644 index 0000000000..78dfb8ed26 --- /dev/null +++ b/packages/megalodon/src/entities/scheduled_status.ts @@ -0,0 +1,10 @@ +/// +/// +namespace Entity { + export type ScheduledStatus = { + id: string; + scheduled_at: string; + params: StatusParams; + media_attachments: Array; + }; +} diff --git a/packages/megalodon/src/entities/source.ts b/packages/megalodon/src/entities/source.ts new file mode 100644 index 0000000000..913b02fda7 --- /dev/null +++ b/packages/megalodon/src/entities/source.ts @@ -0,0 +1,10 @@ +/// +namespace Entity { + export type Source = { + privacy: string | null; + sensitive: boolean | null; + language: string | null; + note: string; + fields: Array; + }; +} diff --git a/packages/megalodon/src/entities/stats.ts b/packages/megalodon/src/entities/stats.ts new file mode 100644 index 0000000000..6471df039a --- /dev/null +++ b/packages/megalodon/src/entities/stats.ts @@ -0,0 +1,7 @@ +namespace Entity { + export type Stats = { + user_count: number; + status_count: number; + domain_count: number; + }; +} diff --git a/packages/megalodon/src/entities/status.ts b/packages/megalodon/src/entities/status.ts new file mode 100644 index 0000000000..f27f728b54 --- /dev/null +++ b/packages/megalodon/src/entities/status.ts @@ -0,0 +1,45 @@ +/// +/// +/// +/// +/// +/// +/// +/// +/// + +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; + mentions: Array; + tags: Array; + card: Card | null; + poll: Poll | null; + application: Application | null; + language: string | null; + pinned: boolean | null; + reactions: Array; + quote: Status | null; + bookmarked: boolean; + }; +} diff --git a/packages/megalodon/src/entities/status_edit.ts b/packages/megalodon/src/entities/status_edit.ts new file mode 100644 index 0000000000..4040b4ff90 --- /dev/null +++ b/packages/megalodon/src/entities/status_edit.ts @@ -0,0 +1,23 @@ +/// +/// +/// +/// +/// +/// +/// +/// +/// + +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; + poll: Poll | null; + }; +} diff --git a/packages/megalodon/src/entities/status_params.ts b/packages/megalodon/src/entities/status_params.ts new file mode 100644 index 0000000000..18908c01c1 --- /dev/null +++ b/packages/megalodon/src/entities/status_params.ts @@ -0,0 +1,12 @@ +namespace Entity { + export type StatusParams = { + text: string; + in_reply_to_id: string | null; + media_ids: Array | null; + sensitive: boolean | null; + spoiler_text: string | null; + visibility: "public" | "unlisted" | "private" | "direct"; + scheduled_at: string | null; + application_id: string; + }; +} diff --git a/packages/megalodon/src/entities/tag.ts b/packages/megalodon/src/entities/tag.ts new file mode 100644 index 0000000000..ccc88aece6 --- /dev/null +++ b/packages/megalodon/src/entities/tag.ts @@ -0,0 +1,10 @@ +/// + +namespace Entity { + export type Tag = { + name: string; + url: string; + history: Array | null; + following?: boolean; + }; +} diff --git a/packages/megalodon/src/entities/token.ts b/packages/megalodon/src/entities/token.ts new file mode 100644 index 0000000000..1583edafb1 --- /dev/null +++ b/packages/megalodon/src/entities/token.ts @@ -0,0 +1,8 @@ +namespace Entity { + export type Token = { + access_token: string; + token_type: string; + scope: string; + created_at: number; + }; +} diff --git a/packages/megalodon/src/entities/urls.ts b/packages/megalodon/src/entities/urls.ts new file mode 100644 index 0000000000..1ee9ed67c9 --- /dev/null +++ b/packages/megalodon/src/entities/urls.ts @@ -0,0 +1,5 @@ +namespace Entity { + export type URLs = { + streaming_api: string; + }; +} diff --git a/packages/megalodon/src/entity.ts b/packages/megalodon/src/entity.ts new file mode 100644 index 0000000000..b73d2b359b --- /dev/null +++ b/packages/megalodon/src/entity.ts @@ -0,0 +1,38 @@ +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// + +export default Entity; diff --git a/packages/megalodon/src/filter_context.ts b/packages/megalodon/src/filter_context.ts new file mode 100644 index 0000000000..4c83cb15f2 --- /dev/null +++ b/packages/megalodon/src/filter_context.ts @@ -0,0 +1,11 @@ +import Entity from "./entity"; + +namespace FilterContext { + export const Home: Entity.FilterContext = "home"; + export const Notifications: Entity.FilterContext = "notifications"; + export const Public: Entity.FilterContext = "public"; + export const Thread: Entity.FilterContext = "thread"; + export const Account: Entity.FilterContext = "account"; +} + +export default FilterContext; diff --git a/packages/megalodon/src/index.ts b/packages/megalodon/src/index.ts new file mode 100644 index 0000000000..758d3a46ad --- /dev/null +++ b/packages/megalodon/src/index.ts @@ -0,0 +1,32 @@ +import Response from "./response"; +import OAuth from "./oauth"; +import { isCancel, RequestCanceledError } from "./cancel"; +import { ProxyConfig } from "./proxy_config"; +import generator, { + detector, + MegalodonInterface, + WebSocketInterface, +} from "./megalodon"; +import Misskey from "./misskey"; +import Entity from "./entity"; +import NotificationType from "./notification"; +import FilterContext from "./filter_context"; +import Converter from "./converter"; + +export { + Response, + OAuth, + RequestCanceledError, + isCancel, + ProxyConfig, + detector, + MegalodonInterface, + WebSocketInterface, + NotificationType, + FilterContext, + Misskey, + Entity, + Converter, +}; + +export default generator; diff --git a/packages/megalodon/src/megalodon.ts b/packages/megalodon/src/megalodon.ts new file mode 100644 index 0000000000..33a5790f67 --- /dev/null +++ b/packages/megalodon/src/megalodon.ts @@ -0,0 +1,1532 @@ +import Response from "./response"; +import OAuth from "./oauth"; +import proxyAgent, { ProxyConfig } from "./proxy_config"; +import Entity from "./entity"; +import axios, { AxiosRequestConfig } from "axios"; +import Misskey from "./misskey"; +import { DEFAULT_UA } from "./default"; + +export interface WebSocketInterface { + start(): void; + stop(): void; + // EventEmitter + on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + removeListener( + event: string | symbol, + listener: (...args: any[]) => void, + ): this; + removeAllListeners(event?: string | symbol): this; +} + +export interface MegalodonInterface { + /** + * Cancel all requests in this instance. + * + * @return void + */ + cancel(): void; + + /** + * First, call createApp to get client_id and client_secret. + * Next, call generateAuthUrl to get authorization url. + * @param client_name Form Data, which is sent to /api/v1/apps + * @param options Form Data, which is sent to /api/v1/apps. and properties should be **snake_case** + */ + registerApp( + client_name: string, + options: Partial<{ + scopes: Array; + redirect_uris: string; + website: string; + }>, + ): Promise; + + /** + * 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; + redirect_uris: string; + website: string; + }>, + ): Promise; + + // ====================================== + // apps + // ====================================== + /** + * GET /api/v1/apps/verify_credentials + * + * @return An Application + */ + verifyAppCredentials(): Promise>; + + // ====================================== + // 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; + + /** + * 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; + + /** + * 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>; + + // ====================================== + // 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>; + /** + * GET /api/v1/accounts/verify_credentials + * + * @return Account. + */ + verifyAccountCredentials(): Promise>; + /** + * 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>; + /** + * GET /api/v1/accounts/:id + * + * @param id The account ID. + * @return An account. + */ + getAccount(id: string): Promise>; + /** + * 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>>; + /** + * 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>>; + /** + * POST /api/v1/pleroma/accounts/:id/subscribe + * + * @param id Target account ID. + * @return Relationship. + */ + subscribeAccount(id: string): Promise>; + /** + * POST /api/v1/pleroma/accounts/:id/unsubscribe + * + * @param id Target account ID. + * @return Relationship. + */ + unsubscribeAccount(id: string): Promise>; + /** + * 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>>; + + /** + * GET /api/v1/accounts/:id/featured_tags + * + * @param id The account ID. + * @return The array of accounts. + */ + getAccountFeaturedTags( + id: string, + ): Promise>>; + + /** + * 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>>; + /** + * GET /api/v1/accounts/:id/lists + * + * @param id The account ID. + * @return The array of lists. + */ + getAccountLists(id: string): Promise>>; + /** + * GET /api/v1/accounts/:id/identity_proofs + * + * @param id The account ID. + * @return Array of IdentityProof + */ + getIdentityProof(id: string): Promise>>; + /** + * 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>; + /** + * POST /api/v1/accounts/:id/unfollow + * + * @param id The account ID. + * @return Relationship + */ + unfollowAccount(id: string): Promise>; + /** + * POST /api/v1/accounts/:id/block + * + * @param id The account ID. + * @return Relationship + */ + blockAccount(id: string): Promise>; + /** + * POST /api/v1/accounts/:id/unblock + * + * @param id The account ID. + * @return RElationship + */ + unblockAccount(id: string): Promise>; + /** + * 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>; + /** + * POST /api/v1/accounts/:id/unmute + * + * @param id The account ID. + * @return Relationship + */ + unmuteAccount(id: string): Promise>; + /** + * POST /api/v1/accounts/:id/pin + * + * @param id The account ID. + * @return Relationship + */ + pinAccount(id: string): Promise>; + /** + * POST /api/v1/accounts/:id/unpin + * + * @param id The account ID. + * @return Relationship + */ + unpinAccount(id: string): Promise>; + /** + * GET /api/v1/accounts/relationships + * + * @param id The account ID. + * @return Relationship + */ + getRelationship(id: string): Promise>; + /** + * Get multiple relationships in one method + * + * @param ids Array of account IDs. + * @return Array of Relationship. + */ + getRelationships( + ids: Array, + ): Promise>>; + /** + * 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>>; + // ====================================== + // 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>>; + // ====================================== + // 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>>; + // ====================================== + // 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>>; + // ====================================== + // 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>>; + // ====================================== + // 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>>; + /** + * POST/api/v1/domain_blocks + * + * @param domain Domain to block. + */ + blockDomain(domain: string): Promise>; + /** + * DELETE /api/v1/domain_blocks + * + * @param domain Domain to unblock + */ + unblockDomain(domain: string): Promise>; + // ====================================== + // accounts/filters + // ====================================== + /** + * GET /api/v1/filters + * + * @return Array of filters. + */ + getFilters(): Promise>>; + /** + * GET /api/v1/filters/:id + * + * @param id The filter ID. + * @return Filter. + */ + getFilter(id: string): Promise>; + /** + * 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, + options?: { + irreversible?: boolean; + whole_word?: boolean; + expires_in?: string; + }, + ): Promise>; + /** + * 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, + options?: { + irreversible?: boolean; + whole_word?: boolean; + expires_in?: string; + }, + ): Promise>; + /** + * DELETE /api/v1/filters/:id + * + * @param id The filter ID. + * @return Removed filter. + */ + deleteFilter(id: string): Promise>; + // ====================================== + // 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; forward?: boolean }, + ): Promise>; + // ====================================== + // accounts/follow_requests + // ====================================== + /** + * GET /api/v1/follow_requests + * + * @param limit Maximum number of results. + * @return Array of account. + */ + getFollowRequests(limit?: number): Promise>>; + /** + * POST /api/v1/follow_requests/:id/authorize + * + * @param id Target account ID. + * @return Relationship. + */ + acceptFollowRequest(id: string): Promise>; + /** + * POST /api/v1/follow_requests/:id/reject + * + * @param id Target account ID. + * @return Relationship. + */ + rejectFollowRequest(id: string): Promise>; + // ====================================== + // 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>>; + // ====================================== + // accounts/featured_tags + // ====================================== + /** + * GET /api/v1/featured_tags + * + * @return Array of featured tag. + */ + getFeaturedTags(): Promise>>; + /** + * POST /api/v1/featured_tags + * + * @param name Target hashtag name. + * @return FeaturedTag. + */ + createFeaturedTag(name: string): Promise>; + /** + * DELETE /api/v1/featured_tags/:id + * + * @param id Target featured tag id. + * @return Empty + */ + deleteFeaturedTag(id: string): Promise>; + /** + * GET /api/v1/featured_tags/suggestions + * + * @return Array of tag. + */ + getSuggestedTags(): Promise>>; + // ====================================== + // accounts/preferences + // ====================================== + /** + * GET /api/v1/preferences + * + * @return Preferences. + */ + getPreferences(): Promise>; + // ====================================== + // accounts/suggestions + // ====================================== + /** + * GET /api/v1/suggestions + * + * @param limit Maximum number of results. + * @return Array of accounts. + */ + getSuggestions(limit?: number): Promise>>; + // ====================================== + // accounts/tags + // ====================================== + getFollowedTags(): Promise>>; + /** + * GET /api/v1/tags/:id + * + * @param id Target hashtag id. + * @return Tag + */ + getTag(id: string): Promise>; + /** + * POST /api/v1/tags/:id/follow + * + * @param id Target hashtag id. + * @return Tag + */ + followTag(id: string): Promise>; + /** + * POST /api/v1/tags/:id/unfollow + * + * @param id Target hashtag id. + * @return Tag + */ + unfollowTag(id: string): Promise>; + // ====================================== + // 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; + poll?: { + options: Array; + 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>; + /** + * GET /api/v1/statuses/:id + * + * @param id The target status id. + * @return Status + */ + getStatus(id: string): Promise>; + /** + 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; + poll?: { + options?: Array; + expires_in?: number; + multiple?: boolean; + hide_totals?: boolean; + }; + }, + ): Promise>; + /** + * DELETE /api/v1/statuses/:id + * + * @param id The target status id. + * @return Status + */ + deleteStatus(id: string): Promise>; + /** + * 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>; + /** + * GET /api/v1/statuses/:id/history + * + * Get status edit history. + * @param id The target status id. + * @return StatusEdit + */ + getStatusHistory(id: string): Promise>>; + /** + * GET /api/v1/statuses/:id/reblogged_by + * + * @param id The target status id. + * @return Array of accounts. + */ + getStatusRebloggedBy(id: string): Promise>>; + /** + * GET /api/v1/statuses/:id/favourited_by + * + * @param id The target status id. + * @return Array of accounts. + */ + getStatusFavouritedBy(id: string): Promise>>; + /** + * POST /api/v1/statuses/:id/favourite + * + * @param id The target status id. + * @return Status. + */ + favouriteStatus(id: string): Promise>; + /** + * POST /api/v1/statuses/:id/unfavourite + * + * @param id The target status id. + * @return Status. + */ + unfavouriteStatus(id: string): Promise>; + /** + * POST /api/v1/statuses/:id/reblog + * + * @param id The target status id. + * @return Status. + */ + reblogStatus(id: string): Promise>; + /** + * POST /api/v1/statuses/:id/unreblog + * + * @param id The target status id. + * @return Status. + */ + unreblogStatus(id: string): Promise>; + /** + * POST /api/v1/statuses/:id/bookmark + * + * @param id The target status id. + * @return Status. + */ + bookmarkStatus(id: string): Promise>; + /** + * POST /api/v1/statuses/:id/unbookmark + * + * @param id The target status id. + * @return Status. + */ + unbookmarkStatus(id: string): Promise>; + /** + * POST /api/v1/statuses/:id/mute + * + * @param id The target status id. + * @return Status + */ + muteStatus(id: string): Promise>; + /** + * POST /api/v1/statuses/:id/unmute + * + * @param id The target status id. + * @return Status + */ + unmuteStatus(id: string): Promise>; + /** + * POST /api/v1/statuses/:id/pin + * @param id The target status id. + * @return Status + */ + pinStatus(id: string): Promise>; + /** + * POST /api/v1/statuses/:id/unpin + * + * @param id The target status id. + * @return Status + */ + unpinStatus(id: string): Promise>; + /** + * 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>; + /** + * 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>; + // ====================================== + // 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>; + /** + * GET /api/v1/media/:id + * + * @param id Target media ID. + * @return Attachment + */ + getMedia(id: string): Promise>; + /** + * 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>; + // ====================================== + // statuses/polls + // ====================================== + /** + * GET /api/v1/polls/:id + * + * @param id Target poll ID. + * @return Poll + */ + getPoll(id: string): Promise>; + /** + * 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): Promise>; + // ====================================== + // 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>>; + /** + * GET /api/v1/scheduled_statuses/:id + * + * @param id Target status ID. + * @return ScheduledStatus. + */ + getScheduledStatus(id: string): Promise>; + /** + * 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>; + /** + * DELETE /api/v1/scheduled_statuses/:id + * + * @param id Target scheduled status ID. + */ + cancelScheduledStatus(id: string): Promise>; + // ====================================== + // 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>>; + /** + * 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>>; + /** + * 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>>; + /** + * 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>>; + /** + * 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>>; + // ====================================== + // 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>>; + /** + * DELETE /api/v1/conversations/:id + * + * @param id Target conversation ID. + */ + deleteConversation(id: string): Promise>; + /** + * POST /api/v1/conversations/:id/read + * + * @param id Target conversation ID. + * @return Conversation. + */ + readConversation(id: string): Promise>; + // ====================================== + // timelines/lists + // ====================================== + /** + * GET /api/v1/lists + * + * @return Array of lists. + */ + getLists(): Promise>>; + /** + * GET /api/v1/lists/:id + * + * @param id Target list ID. + * @return List. + */ + getList(id: string): Promise>; + /** + * POST /api/v1/lists + * + * @param title List name. + * @return List. + */ + createList(title: string): Promise>; + /** + * PUT /api/v1/lists/:id + * + * @param id Target list ID. + * @param title New list name. + * @return List. + */ + updateList(id: string, title: string): Promise>; + /** + * DELETE /api/v1/lists/:id + * + * @param id Target list ID. + */ + deleteList(id: string): Promise>; + /** + * 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>>; + /** + * 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, + ): Promise>; + /** + * 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, + ): Promise>; + // ====================================== + // 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): Promise>; + /** + * 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>; + // ====================================== + // 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; + account_id?: string; + }): Promise>>; + /** + * GET /api/v1/notifications/:id + * + * @param id Target notification ID. + * @return Notification. + */ + getNotification(id: string): Promise>; + /** + * POST /api/v1/notifications/clear + */ + dismissNotifications(): Promise>; + /** + * POST /api/v1/notifications/:id/dismiss + * + * @param id Target notification ID. + */ + dismissNotification(id: string): Promise>; + /** + * 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> + >; + // ====================================== + // 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>; + /** + * GET /api/v1/push/subscription + * + * @return PushSubscription. + */ + getPushSubscription(): Promise>; + /** + * 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>; + /** + * DELETE /api/v1/push/subscription + */ + deletePushSubscription(): Promise>; + // ====================================== + // 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>; + + // ====================================== + // instance + // ====================================== + /** + * GET /api/v1/instance + */ + getInstance(): Promise>; + + /** + * GET /api/v1/instance/peers + */ + getInstancePeers(): Promise>>; + + /** + * GET /api/v1/instance/activity + */ + getInstanceActivity(): Promise>>; + + // ====================================== + // instance/trends + // ====================================== + /** + * GET /api/v1/trends + * + * @param limit Maximum number of results to return. Defaults to 10. + */ + getInstanceTrends( + limit?: number | null, + ): Promise>>; + + // ====================================== + // 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>>; + + // ====================================== + // instance/custom_emojis + // ====================================== + /** + * GET /api/v1/custom_emojis + * + * @return Array of emojis. + */ + getInstanceCustomEmojis(): Promise>>; + + // ====================================== + // 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>>; + + /** + * POST /api/v1/announcements/:id/dismiss + */ + dismissInstanceAnnouncement(id: string): Promise>; + + // ====================================== + // Emoji reactions + // ====================================== + createEmojiReaction( + id: string, + emoji: string, + ): Promise>; + deleteEmojiReaction( + id: string, + emoji: string, + ): Promise>; + getEmojiReactions(id: string): Promise>>; + getEmojiReaction( + id: string, + emoji: string, + ): Promise>; + + // ====================================== + // 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(url + "/api/v1/instance", options); + if (res.data.version.includes("Pleroma")) { + return "pleroma"; + } else { + return "mastodon"; + } + } catch (err) { + await axios.post<{}>(url + "/api/meta", {}, options); + return "misskey"; + } +}; + +/** + * Get client for each SNS according to megalodon interface. + * + * @param baseUrl hostname or base URL. + * @param accessToken access token from OAuth2 authorization + * @param userAgent UserAgent is specified in header on request. + * @param proxyConfig Proxy setting, or set false if don't use proxy. + * @return Client instance for each SNS you specified. + */ +const generator = ( + baseUrl: string, + accessToken: string | null = null, + userAgent: string | null = null, + proxyConfig: ProxyConfig | false = false, +): MegalodonInterface => + new Misskey(baseUrl, accessToken, userAgent, proxyConfig); + +export default generator; diff --git a/packages/megalodon/src/misskey.ts b/packages/megalodon/src/misskey.ts new file mode 100644 index 0000000000..25922a2ffc --- /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; + redirect_uris: string; + website: string; + }> = { + scopes: MisskeyAPI.DEFAULT_SCOPE, + redirect_uris: this.baseUrl, + }, + ): Promise { + 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; + redirect_uris: string; + website: string; + }> = { + scopes: MisskeyAPI.DEFAULT_SCOPE, + redirect_uris: this.baseUrl, + }, + ): Promise { + const redirect_uris = options.redirect_uris || this.baseUrl; + const scopes = options.scopes || MisskeyAPI.DEFAULT_SCOPE; + + const params: { + name: string; + description: string; + permission: Array; + 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("/api/app/create", params) + .then((res: Response) => { + 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 { + return this.client + .post("/api/auth/session/generate", { + appSecret: clientSecret, + }) + .then((res: Response) => res.data); + } + + // ====================================== + // apps + // ====================================== + public async verifyAppCredentials(): Promise> { + 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 { + return this.client + .post("/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 { + 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> { + 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> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + /** + * POST /api/i + */ + public async verifyAccountCredentials(): Promise> { + return this.client + .post("/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> { + 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("/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> { + return this.client + .post("/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> { + return this.client + .post("/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>> { + const accountCache = this.getFreshAccountCache(); + + if (options?.pinned) { + return this.client + .post("/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>("/api/users/notes", params) + .then(async (res) => { + const statuses: Array = 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>> { + 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>("/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> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async unsubscribeAccount( + _id: string, + ): Promise> { + 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>> { + 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>("/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>> { + let params = { + userId: id, + }; + if (options) { + if (options.limit) { + params = Object.assign(params, { + limit: options.limit <= 100 ? options.limit : 100, + }); + } + } + return this.client + .post>("/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>> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async getIdentityProof( + _id: string, + ): Promise>> { + 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> { + await this.client.post<{}>("/api/following/create", { + userId: id, + }); + return this.client + .post("/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> { + await this.client.post<{}>("/api/following/delete", { + userId: id, + }); + return this.client + .post("/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> { + await this.client.post<{}>("/api/blocking/create", { + userId: id, + }); + return this.client + .post("/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> { + await this.client.post<{}>("/api/blocking/delete", { + userId: id, + }); + return this.client + .post("/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> { + await this.client.post<{}>("/api/mute/create", { + userId: id, + }); + return this.client + .post("/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> { + await this.client.post<{}>("/api/mute/delete", { + userId: id, + }); + return this.client + .post("/api/users/relation", { + userId: id, + }) + .then((res) => { + return Object.assign(res, { + data: this.converter.relation(res.data), + }); + }); + } + + public async pinAccount(_id: string): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async unpinAccount( + _id: string, + ): Promise> { + 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> { + return this.client + .post("/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, + ): Promise>> { + 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>> { + 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>("/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>> { + 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>("/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>> { + const userId = await this.client + .post("/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>> { + 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>("/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>> { + 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>("/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>> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async blockDomain(_domain: string): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async unblockDomain(_domain: string): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + // ====================================== + // accounts/filters + // ====================================== + public async getFilters(): Promise>> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async getFilter(_id: string): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async createFilter( + _phrase: string, + _context: Array, + _options?: { + irreversible?: boolean; + whole_word?: boolean; + expires_in?: string; + }, + ): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async updateFilter( + _id: string, + _phrase: string, + _context: Array, + _options?: { + irreversible?: boolean; + whole_word?: boolean; + expires_in?: string; + }, + ): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async deleteFilter(_id: string): Promise> { + 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; + forward?: boolean; + }, + ): Promise> { + 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>> { + return this.client + .post>( + "/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> { + await this.client.post<{}>("/api/following/requests/accept", { + userId: id, + }); + return this.client + .post("/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> { + await this.client.post<{}>("/api/following/requests/reject", { + userId: id, + }); + return this.client + .post("/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>> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + // ====================================== + // accounts/featured_tags + // ====================================== + public async getFeaturedTags(): Promise>> { + return this.getAccountFeaturedTags(); + } + + public async getAccountFeaturedTags(): Promise< + Response> + > { + 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> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async deleteFeaturedTag(_id: string): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async getSuggestedTags(): Promise>> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + // ====================================== + // accounts/preferences + // ====================================== + public async getPreferences(): Promise> { + return this.client + .post("/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>> { + let params = {}; + if (limit) { + params = Object.assign(params, { + limit: limit, + }); + } + return this.client + .post>( + "/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>> { + 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> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async followTag(_id: string): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async unfollowTag(_id: string): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + // ====================================== + // statuses + // ====================================== + public async postStatus( + status: string, + options?: { + media_ids?: Array; + poll?: { + options: Array; + 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> { + 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("/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> { + return this.client + .post("/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 { + 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 { + 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 { + return this.client + .post("/api/notes/state", { + noteId: id, + }) + .then((p) => p.data.isFavorited ?? false); + } + + public async addUserDetailsToStatus( + status: Entity.Status, + cache: AccountCache, + ): Promise { + 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 { + 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, + `@${m.acct}`, + ); + } + return status; + } + + public async getMentions( + text: string, + origin: string | null, + cache: AccountCache, + ): Promise { + const mentions: Entity.Mention[] = []; + + if (text == undefined) return mentions; + + const mentionMatch = text.matchAll( + /(?<=^|\s)@(?[a-zA-Z0-9_]+)(?:@(?[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 { + 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 { + 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; + poll?: { + options?: Array; + expires_in?: number; + multiple?: boolean; + hide_totals?: boolean; + }; + }, + ): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + /** + * POST /api/notes/delete + */ + public async deleteStatus(id: string): Promise> { + 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> { + 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>("/api/notes/children", params) + .then(async (res) => { + const accountCache = this.getFreshAccountCache(); + const conversation = await this.client.post< + Array + >("/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>> { + // 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>> { + return this.client + .post>("/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>> { + return this.client + .post>("/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> { + return this.createEmojiReaction(id, await this.getDefaultFavoriteEmoji()); + } + + private async getDefaultFavoriteEmoji(): Promise { + // 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>("/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("/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> { + // 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> { + return this.client + .post("/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> { + await this.client.post<{}>("/api/notes/unrenote", { + noteId: id, + }); + return this.client + .post("/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> { + await this.client.post<{}>("/api/notes/favorites/create", { + noteId: id, + }); + return this.client + .post("/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> { + await this.client.post<{}>("/api/notes/favorites/delete", { + noteId: id, + }); + return this.client + .post("/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> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async unmuteStatus(_id: string): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + /** + * POST /api/i/pin + */ + public async pinStatus(id: string): Promise> { + await this.client.post<{}>("/api/i/pin", { + noteId: id, + }); + return this.client + .post("/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> { + await this.client.post<{}>("/api/i/unpin", { + noteId: id, + }); + return this.client + .post("/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> { + await this.client.post<{}>("/api/notes/reactions/create", { + noteId: id, + reaction: this.reactionName(name), + }); + return this.client + .post("/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> { + await this.client.post<{}>("/api/notes/reactions/delete", { + noteId: id, + reaction: this.reactionName(name), + }); + return this.client + .post("/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> { + 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( + "/api/drive/files/create", + formData, + headers, + ) + .then((res) => ({ ...res, data: this.converter.file(res.data) })); + } + + public async getMedia(id: string): Promise> { + const res = await this.client.post( + "/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> { + 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("/api/drive/files/update", params) + .then((res) => ({ ...res, data: this.converter.file(res.data) })); + } + + // ====================================== + // statuses/polls + // ====================================== + public async getPoll(id: string): Promise> { + 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, + ): Promise> { + 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("/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>> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async getScheduledStatus( + _id: string, + ): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async scheduleStatus( + _id: string, + _scheduled_at?: string | null, + ): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async cancelScheduledStatus(_id: string): Promise> { + 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>> { + 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>("/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>> { + 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>("/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>> { + 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>("/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>> { + 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>("/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>> { + 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>( + "/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>> { + 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>("/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> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async readConversation( + _id: string, + ): Promise> { + 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>> { + return this.client + .post>("/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> { + return this.client + .post("/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> { + return this.client + .post("/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> { + return this.client + .post("/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> { + 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>> { + const res = await this.client.post( + "/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, + ): Promise> { + 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, + ): Promise> { + return this.client.post<{}>("/api/users/lists/pull", { + listId: id, + userId: account_ids[0], + }); + } + + // ====================================== + // timelines/markers + // ====================================== + public async getMarkers( + _timeline: Array, + ): Promise> { + 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> { + 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; + account_id?: string; + }): Promise>> { + 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>( + "/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> { + 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> { + return this.client.post<{}>("/api/notifications/mark-all-as-read"); + } + + public async dismissNotification(_id: string): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async readNotifications(_options: { + id?: string; + max_id?: string; + }): Promise>> { + 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> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async getPushSubscription(): Promise< + Response + > { + 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> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + /** + * DELETE /api/v1/push/subscription + */ + public async deletePushSubscription(): Promise> { + 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> { + 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( + /^@(?[a-zA-Z0-9_]+)(?:@(?[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( + "/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>( + "/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>("/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>("/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> { + const meta = await this.client + .post("/api/meta") + .then((res) => res.data); + return this.client + .post("/api/stats") + .then((res) => ({ ...res, data: this.converter.meta(meta, res.data) })); + } + + public async getInstancePeers(): Promise>> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public async getInstanceActivity(): Promise< + Response> + > { + 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>> { + return this.client + .post>("/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>> { + 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> + > { + return this.client + .post("/api/meta") + .then((res) => ({ + ...res, + data: res.data.emojis.map((e) => this.converter.emoji(e)), + })); + } + + // ====================================== + // instance/announcements + // ====================================== + public async getInstanceAnnouncements( + with_dismissed?: boolean | null, + ): Promise>> { + let params = {}; + if (with_dismissed) { + params = Object.assign(params, { + withUnreads: with_dismissed, + }); + } + return this.client + .post>("/api/announcements", params) + .then((res) => ({ + ...res, + data: res.data.map((t) => this.converter.announcement(t)), + })); + } + + public async dismissInstanceAnnouncement(id: string): Promise> { + 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> { + await this.client.post<{}>("/api/notes/reactions/create", { + noteId: id, + reaction: emoji, + }); + return this.client + .post("/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> { + await this.client.post<{}>("/api/notes/reactions/delete", { + noteId: id, + }); + return this.client + .post("/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>> { + return this.client + .post>("/api/notes/reactions", { + noteId: id, + }) + .then((res) => ({ + ...res, + data: this.converter.reactions(res.data), + })); + } + + public async getEmojiReaction( + _id: string, + _emoji: string, + ): Promise> { + return new Promise((_, reject) => { + const err = new NoImplementedError("misskey does not support"); + reject(err); + }); + } + + public userSocket(): WebSocketInterface { + return this.client.socket("user"); + } + + public publicSocket(): WebSocketInterface { + return this.client.socket("globalTimeline"); + } + + public localSocket(): WebSocketInterface { + return this.client.socket("localTimeline"); + } + + public tagSocket(_tag: string): WebSocketInterface { + throw new NoImplementedError("TODO: implement"); + } + + public listSocket(list_id: string): WebSocketInterface { + return this.client.socket("list", list_id); + } + + public directSocket(): WebSocketInterface { + return this.client.socket("conversation"); + } +} diff --git a/packages/megalodon/src/misskey/api_client.ts b/packages/megalodon/src/misskey/api_client.ts new file mode 100644 index 0000000000..47932cbf19 --- /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, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(/`/g, "`") + .replace(/\r?\n/g, "
"); + + 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, "
") ?? "", + url: acctUrl, + avatar: u.avatarUrl, + avatar_static: u.avatarUrl, + header: u.bannerUrl ?? this.plcUrl, + header_static: u.bannerUrl ?? this.plcUrl, + emojis: 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, + r: { [key: string]: number }, + myReaction?: string, + ): Array => { + // Map of emoji shortcodes to image URLs. + const emojiUrls = new Map( + 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, + ): Array => { + const result: Array = []; + 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 = [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: `

${this.escapeMFM(a.title)}

${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( + path: string, + params?: any, + headers?: { [key: string]: string }, + ): Promise>; + 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( + path: string, + params: any = {}, + headers: { [key: string]: string } = {}, + ): Promise> { + 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(this.baseUrl + path, bodyParams, options) + .then((resp: AxiosResponse) => { + const res: Response = { + data: resp.data, + status: resp.status, + statusText: resp.statusText, + headers: resp.headers, + }; + return res; + }); + } + + /** + * Cancel all requests in this instance. + * @returns void + */ + public cancel(): void { + return this.abortController.abort(); + } + + /** + * Get connection and receive websocket connection for Misskey API. + * + * @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list. + * @param listId This parameter is required only list channel. + */ + public socket( + channel: + | "user" + | "localTimeline" + | "hybridTimeline" + | "globalTimeline" + | "conversation" + | "list", + listId?: string, + ): WebSocket { + if (!this.accessToken) { + throw new Error("accessToken is required"); + } + const url = `${this.baseUrl}/streaming`; + const streaming = new WebSocket( + url, + channel, + this.accessToken, + listId, + this.userAgent, + this.proxyConfig, + this.converter, + ); + process.nextTick(() => { + streaming.start(); + }); + return streaming; + } + } +} + +export default MisskeyAPI; diff --git a/packages/megalodon/src/misskey/entities/GetAll.ts b/packages/megalodon/src/misskey/entities/GetAll.ts new file mode 100644 index 0000000000..94ace2f184 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/GetAll.ts @@ -0,0 +1,6 @@ +namespace MisskeyEntity { + export type GetAll = { + tutorial: number; + defaultNoteVisibility: "public" | "home" | "followers" | "specified"; + }; +} diff --git a/packages/megalodon/src/misskey/entities/announcement.ts b/packages/megalodon/src/misskey/entities/announcement.ts new file mode 100644 index 0000000000..7594ba7efc --- /dev/null +++ b/packages/megalodon/src/misskey/entities/announcement.ts @@ -0,0 +1,10 @@ +namespace MisskeyEntity { + export type Announcement = { + id: string; + createdAt: string; + updatedAt: string; + text: string; + title: string; + isRead?: boolean; + }; +} diff --git a/packages/megalodon/src/misskey/entities/app.ts b/packages/megalodon/src/misskey/entities/app.ts new file mode 100644 index 0000000000..5924060d81 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/app.ts @@ -0,0 +1,9 @@ +namespace MisskeyEntity { + export type App = { + id: string; + name: string; + callbackUrl: string; + permission: Array; + secret: string; + }; +} diff --git a/packages/megalodon/src/misskey/entities/blocking.ts b/packages/megalodon/src/misskey/entities/blocking.ts new file mode 100644 index 0000000000..3e56790a7b --- /dev/null +++ b/packages/megalodon/src/misskey/entities/blocking.ts @@ -0,0 +1,10 @@ +/// + +namespace MisskeyEntity { + export type Blocking = { + id: string; + createdAt: string; + blockeeId: string; + blockee: UserDetail; + }; +} diff --git a/packages/megalodon/src/misskey/entities/createdNote.ts b/packages/megalodon/src/misskey/entities/createdNote.ts new file mode 100644 index 0000000000..235f7063fb --- /dev/null +++ b/packages/megalodon/src/misskey/entities/createdNote.ts @@ -0,0 +1,7 @@ +/// + +namespace MisskeyEntity { + export type CreatedNote = { + createdNote: Note; + }; +} diff --git a/packages/megalodon/src/misskey/entities/emoji.ts b/packages/megalodon/src/misskey/entities/emoji.ts new file mode 100644 index 0000000000..d320760e91 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/emoji.ts @@ -0,0 +1,9 @@ +namespace MisskeyEntity { + export type Emoji = { + name: string; + host: string | null; + url: string; + aliases: Array; + category: string; + }; +} diff --git a/packages/megalodon/src/misskey/entities/favorite.ts b/packages/megalodon/src/misskey/entities/favorite.ts new file mode 100644 index 0000000000..ba948f2e73 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/favorite.ts @@ -0,0 +1,10 @@ +/// + +namespace MisskeyEntity { + export type Favorite = { + id: string; + createdAt: string; + noteId: string; + note: Note; + }; +} diff --git a/packages/megalodon/src/misskey/entities/field.ts b/packages/megalodon/src/misskey/entities/field.ts new file mode 100644 index 0000000000..8bbb2d7c42 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/field.ts @@ -0,0 +1,7 @@ +namespace MisskeyEntity { + export type Field = { + name: string; + value: string; + verified?: string; + }; +} diff --git a/packages/megalodon/src/misskey/entities/file.ts b/packages/megalodon/src/misskey/entities/file.ts new file mode 100644 index 0000000000..e823dde1be --- /dev/null +++ b/packages/megalodon/src/misskey/entities/file.ts @@ -0,0 +1,20 @@ +namespace MisskeyEntity { + export type File = { + id: string; + createdAt: string; + name: string; + type: string; + md5: string; + size: number; + isSensitive: boolean; + properties: { + width: number; + height: number; + avgColor: string; + }; + url: string; + thumbnailUrl: string; + comment: string; + blurhash: string; + }; +} diff --git a/packages/megalodon/src/misskey/entities/followRequest.ts b/packages/megalodon/src/misskey/entities/followRequest.ts new file mode 100644 index 0000000000..60bd0e0abc --- /dev/null +++ b/packages/megalodon/src/misskey/entities/followRequest.ts @@ -0,0 +1,9 @@ +/// + +namespace MisskeyEntity { + export type FollowRequest = { + id: string; + follower: User; + followee: User; + }; +} diff --git a/packages/megalodon/src/misskey/entities/follower.ts b/packages/megalodon/src/misskey/entities/follower.ts new file mode 100644 index 0000000000..34ae825519 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/follower.ts @@ -0,0 +1,11 @@ +/// + +namespace MisskeyEntity { + export type Follower = { + id: string; + createdAt: string; + followeeId: string; + followerId: string; + follower: UserDetail; + }; +} diff --git a/packages/megalodon/src/misskey/entities/following.ts b/packages/megalodon/src/misskey/entities/following.ts new file mode 100644 index 0000000000..6cbc8f1c39 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/following.ts @@ -0,0 +1,11 @@ +/// + +namespace MisskeyEntity { + export type Following = { + id: string; + createdAt: string; + followeeId: string; + followerId: string; + followee: UserDetail; + }; +} diff --git a/packages/megalodon/src/misskey/entities/hashtag.ts b/packages/megalodon/src/misskey/entities/hashtag.ts new file mode 100644 index 0000000000..3ec4d6675b --- /dev/null +++ b/packages/megalodon/src/misskey/entities/hashtag.ts @@ -0,0 +1,7 @@ +namespace MisskeyEntity { + export type Hashtag = { + tag: string; + chart: Array; + usersCount: number; + }; +} diff --git a/packages/megalodon/src/misskey/entities/list.ts b/packages/megalodon/src/misskey/entities/list.ts new file mode 100644 index 0000000000..60706592a4 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/list.ts @@ -0,0 +1,8 @@ +namespace MisskeyEntity { + export type List = { + id: string; + createdAt: string; + name: string; + userIds: Array; + }; +} diff --git a/packages/megalodon/src/misskey/entities/meta.ts b/packages/megalodon/src/misskey/entities/meta.ts new file mode 100644 index 0000000000..97827fe8fd --- /dev/null +++ b/packages/megalodon/src/misskey/entities/meta.ts @@ -0,0 +1,18 @@ +/// + +namespace MisskeyEntity { + export type Meta = { + maintainerName: string; + maintainerEmail: string; + name: string; + version: string; + uri: string; + description: string; + langs: Array; + disableRegistration: boolean; + disableLocalTimeline: boolean; + bannerUrl: string; + maxNoteTextLength: 3000; + emojis: Array; + }; +} diff --git a/packages/megalodon/src/misskey/entities/mute.ts b/packages/megalodon/src/misskey/entities/mute.ts new file mode 100644 index 0000000000..7975b3d315 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/mute.ts @@ -0,0 +1,10 @@ +/// + +namespace MisskeyEntity { + export type Mute = { + id: string; + createdAt: string; + muteeId: string; + mutee: UserDetail; + }; +} diff --git a/packages/megalodon/src/misskey/entities/note.ts b/packages/megalodon/src/misskey/entities/note.ts new file mode 100644 index 0000000000..64a0a50785 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/note.ts @@ -0,0 +1,32 @@ +/// +/// +/// +/// + +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; + fileIds: Array; + files: Array; + replyId: string | null; + renoteId: string | null; + uri?: string; + reply?: Note; + renote?: Note; + viaMobile?: boolean; + tags?: Array; + poll?: Poll; + mentions?: Array; + myReaction?: string; + }; +} diff --git a/packages/megalodon/src/misskey/entities/notification.ts b/packages/megalodon/src/misskey/entities/notification.ts new file mode 100644 index 0000000000..7ecb911537 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/notification.ts @@ -0,0 +1,17 @@ +/// +/// + +namespace MisskeyEntity { + export type Notification = { + id: string; + createdAt: string; + // https://github.com/syuilo/misskey/blob/056942391aee135eb6c77aaa63f6ed5741d701a6/src/models/entities/notification.ts#L50-L62 + type: NotificationType; + userId: string; + user: User; + note?: Note; + reaction?: string; + }; + + export type NotificationType = string; +} diff --git a/packages/megalodon/src/misskey/entities/poll.ts b/packages/megalodon/src/misskey/entities/poll.ts new file mode 100644 index 0000000000..9f6bfa40d2 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/poll.ts @@ -0,0 +1,13 @@ +namespace MisskeyEntity { + export type Choice = { + text: string; + votes: number; + isVoted: boolean; + }; + + export type Poll = { + multiple: boolean; + expiresAt: string; + choices: Array; + }; +} diff --git a/packages/megalodon/src/misskey/entities/reaction.ts b/packages/megalodon/src/misskey/entities/reaction.ts new file mode 100644 index 0000000000..b35a25bfb5 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/reaction.ts @@ -0,0 +1,11 @@ +/// + +namespace MisskeyEntity { + export type Reaction = { + id: string; + createdAt: string; + user: User; + url?: string; + type: string; + }; +} diff --git a/packages/megalodon/src/misskey/entities/relation.ts b/packages/megalodon/src/misskey/entities/relation.ts new file mode 100644 index 0000000000..6db4a1b167 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/relation.ts @@ -0,0 +1,12 @@ +namespace MisskeyEntity { + export type Relation = { + id: string; + isFollowing: boolean; + hasPendingFollowRequestFromYou: boolean; + hasPendingFollowRequestToYou: boolean; + isFollowed: boolean; + isBlocking: boolean; + isBlocked: boolean; + isMuted: boolean; + }; +} diff --git a/packages/megalodon/src/misskey/entities/session.ts b/packages/megalodon/src/misskey/entities/session.ts new file mode 100644 index 0000000000..572333ff0b --- /dev/null +++ b/packages/megalodon/src/misskey/entities/session.ts @@ -0,0 +1,6 @@ +namespace MisskeyEntity { + export type Session = { + token: string; + url: string; + }; +} diff --git a/packages/megalodon/src/misskey/entities/state.ts b/packages/megalodon/src/misskey/entities/state.ts new file mode 100644 index 0000000000..62d60ce282 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/state.ts @@ -0,0 +1,7 @@ +namespace MisskeyEntity { + export type State = { + isFavorited: boolean; + isMutedThread: boolean; + isWatching: boolean; + }; +} diff --git a/packages/megalodon/src/misskey/entities/stats.ts b/packages/megalodon/src/misskey/entities/stats.ts new file mode 100644 index 0000000000..9832a0ad8a --- /dev/null +++ b/packages/megalodon/src/misskey/entities/stats.ts @@ -0,0 +1,9 @@ +namespace MisskeyEntity { + export type Stats = { + notesCount: number; + originalNotesCount: number; + usersCount: number; + originalUsersCount: number; + instances: number; + }; +} diff --git a/packages/megalodon/src/misskey/entities/user.ts b/packages/megalodon/src/misskey/entities/user.ts new file mode 100644 index 0000000000..96610f6e6d --- /dev/null +++ b/packages/megalodon/src/misskey/entities/user.ts @@ -0,0 +1,13 @@ +/// + +namespace MisskeyEntity { + export type User = { + id: string; + name: string; + username: string; + host: string | null; + avatarUrl: string; + avatarColor: string; + emojis: Array; + }; +} diff --git a/packages/megalodon/src/misskey/entities/userDetail.ts b/packages/megalodon/src/misskey/entities/userDetail.ts new file mode 100644 index 0000000000..0f5bd5f644 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/userDetail.ts @@ -0,0 +1,34 @@ +/// +/// +/// + +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; + 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; + pinnedNotes?: Array; + fields: Array; + }; +} diff --git a/packages/megalodon/src/misskey/entities/userDetailMe.ts b/packages/megalodon/src/misskey/entities/userDetailMe.ts new file mode 100644 index 0000000000..272e65ffa4 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/userDetailMe.ts @@ -0,0 +1,36 @@ +/// +/// +/// + +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; + 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; + pinnedNotes?: Array; + fields: Array; + alwaysMarkNsfw: boolean; + lang: string | null; + }; +} diff --git a/packages/megalodon/src/misskey/entities/userkey.ts b/packages/megalodon/src/misskey/entities/userkey.ts new file mode 100644 index 0000000000..921af65536 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/userkey.ts @@ -0,0 +1,8 @@ +/// + +namespace MisskeyEntity { + export type UserKey = { + accessToken: string; + user: User; + }; +} diff --git a/packages/megalodon/src/misskey/entity.ts b/packages/megalodon/src/misskey/entity.ts new file mode 100644 index 0000000000..72a80f9d96 --- /dev/null +++ b/packages/megalodon/src/misskey/entity.ts @@ -0,0 +1,28 @@ +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// + +export default MisskeyEntity; diff --git a/packages/megalodon/src/misskey/notification.ts b/packages/megalodon/src/misskey/notification.ts new file mode 100644 index 0000000000..eb7c2d23d8 --- /dev/null +++ b/packages/megalodon/src/misskey/notification.ts @@ -0,0 +1,18 @@ +import MisskeyEntity from "./entity"; + +namespace MisskeyNotificationType { + export const Follow: MisskeyEntity.NotificationType = "follow"; + export const Mention: MisskeyEntity.NotificationType = "mention"; + export const Reply: MisskeyEntity.NotificationType = "reply"; + export const Renote: MisskeyEntity.NotificationType = "renote"; + export const Quote: MisskeyEntity.NotificationType = "quote"; + export const Reaction: MisskeyEntity.NotificationType = "favourite"; + export const PollEnded: MisskeyEntity.NotificationType = "pollEnded"; + export const ReceiveFollowRequest: MisskeyEntity.NotificationType = + "receiveFollowRequest"; + export const FollowRequestAccepted: MisskeyEntity.NotificationType = + "followRequestAccepted"; + export const GroupInvited: MisskeyEntity.NotificationType = "groupInvited"; +} + +export default MisskeyNotificationType; diff --git a/packages/megalodon/src/misskey/web_socket.ts b/packages/megalodon/src/misskey/web_socket.ts new file mode 100644 index 0000000000..677f2049d3 --- /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://firefish.io/streaming + * @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list. + * @param accessToken The access token. + * @param listId This parameter is required when you specify list as channel. + */ + constructor( + url: string, + channel: + | "user" + | "localTimeline" + | "hybridTimeline" + | "globalTimeline" + | "conversation" + | "list", + accessToken: string, + listId: string | undefined, + userAgent: string, + proxyConfig: ProxyConfig | false = false, + converter: MisskeyAPI.Converter, + ) { + super(); + this.url = url; + this.parser = new Parser(); + this.channel = channel; + this.headers = { + "User-Agent": userAgent, + }; + if (listId === undefined) { + this.listId = null; + } else { + this.listId = listId; + } + this.proxyConfig = proxyConfig; + this._accessToken = accessToken; + this._reconnectInterval = 10000; + this._reconnectMaxAttempts = Infinity; + this._reconnectCurrentAttempts = 0; + this._connectionClosed = false; + this._channelID = uuid(); + this._pongReceivedTimestamp = dayjs(); + this._converter = converter; + } + + /** + * Start websocket connection. + */ + public start() { + this._connectionClosed = false; + this._resetRetryParams(); + this._startWebSocketConnection(); + } + + private baseUrlToHost(baseUrl: string): string { + return baseUrl.replace("https://", ""); + } + + /** + * Reset connection and start new websocket connection. + */ + private _startWebSocketConnection() { + this._resetConnection(); + this._setupParser(); + this._client = this._connect(); + this._bindSocket(this._client); + } + + /** + * Stop current connection. + */ + public stop() { + this._connectionClosed = true; + this._resetConnection(); + this._resetRetryParams(); + } + + /** + * Clean up current connection, and listeners. + */ + private _resetConnection() { + if (this._client) { + this._client.close(1000); + this._client.removeAllListeners(); + this._client = null; + } + + if (this.parser) { + this.parser.removeAllListeners(); + } + } + + /** + * Resets the parameters used in reconnect. + */ + private _resetRetryParams() { + this._reconnectCurrentAttempts = 0; + } + + /** + * Connect to the endpoint. + */ + private _connect(): WS { + let options: WS.ClientOptions = { + headers: this.headers, + }; + if (this.proxyConfig) { + options = Object.assign(options, { + agent: proxyAgent(this.proxyConfig), + }); + } + const cli: WS = new WS(`${this.url}?i=${this._accessToken}`, options); + return cli; + } + + /** + * Connect specified channels in websocket. + */ + private _channel() { + if (!this._client) { + return; + } + switch (this.channel) { + case "conversation": + this._client.send( + JSON.stringify({ + type: "connect", + body: { + channel: "main", + id: this._channelID, + }, + }), + ); + break; + case "user": + this._client.send( + JSON.stringify({ + type: "connect", + body: { + channel: "main", + id: this._channelID, + }, + }), + ); + this._client.send( + JSON.stringify({ + type: "connect", + body: { + channel: "homeTimeline", + id: this._channelID, + }, + }), + ); + break; + case "list": + this._client.send( + JSON.stringify({ + type: "connect", + body: { + channel: "userList", + id: this._channelID, + params: { + listId: this.listId, + }, + }, + }), + ); + break; + default: + this._client.send( + JSON.stringify({ + type: "connect", + body: { + channel: this.channel, + id: this._channelID, + }, + }), + ); + break; + } + } + + /** + * Reconnects to the same endpoint. + */ + + private _reconnect() { + setTimeout(() => { + // Skip reconnect when client is connecting. + // https://github.com/websockets/ws/blob/7.2.1/lib/websocket.js#L365 + if (this._client && this._client.readyState === WS.CONNECTING) { + return; + } + + if (this._reconnectCurrentAttempts < this._reconnectMaxAttempts) { + this._reconnectCurrentAttempts++; + this._clearBinding(); + if (this._client) { + // In reconnect, we want to close the connection immediately, + // because recoonect is necessary when some problems occur. + this._client.terminate(); + } + // Call connect methods + console.log("Reconnecting"); + this._client = this._connect(); + this._bindSocket(this._client); + } + }, this._reconnectInterval); + } + + /** + * Clear binding event for websocket client. + */ + private _clearBinding() { + if (this._client) { + this._client.removeAllListeners("close"); + this._client.removeAllListeners("pong"); + this._client.removeAllListeners("open"); + this._client.removeAllListeners("message"); + this._client.removeAllListeners("error"); + } + } + + /** + * Bind event for web socket client. + * @param client A WebSocket instance. + */ + private _bindSocket(client: WS) { + client.on("close", (code: number, _reason: Buffer) => { + if (code === 1000) { + this.emit("close", {}); + } else { + console.log(`Closed connection with ${code}`); + if (!this._connectionClosed) { + this._reconnect(); + } + } + }); + client.on("pong", () => { + this._pongWaiting = false; + this.emit("pong", {}); + this._pongReceivedTimestamp = dayjs(); + // It is required to anonymous function since get this scope in checkAlive. + setTimeout( + () => this._checkAlive(this._pongReceivedTimestamp), + this._heartbeatInterval, + ); + }); + client.on("open", () => { + this.emit("connect", {}); + this._channel(); + // Call first ping event. + setTimeout(() => { + client.ping(""); + }, 10000); + }); + client.on("message", (data: WS.Data, isBinary: boolean) => { + this.parser.parse(data, isBinary, this._channelID); + }); + client.on("error", (err: Error) => { + this.emit("error", err); + }); + } + + /** + * Set up parser when receive message. + */ + private _setupParser() { + this.parser.on("update", (note: MisskeyAPI.Entity.Note) => { + this.emit( + "update", + this._converter.note(note, this.baseUrlToHost(this.url)), + ); + }); + this.parser.on( + "notification", + (notification: MisskeyAPI.Entity.Notification) => { + this.emit( + "notification", + this._converter.notification( + notification, + this.baseUrlToHost(this.url), + ), + ); + }, + ); + this.parser.on("conversation", (note: MisskeyAPI.Entity.Note) => { + this.emit( + "conversation", + this._converter.noteToConversation(note, this.baseUrlToHost(this.url)), + ); + }); + this.parser.on("error", (err: Error) => { + this.emit("parser-error", err); + }); + } + + /** + * Call ping and wait to pong. + */ + private _checkAlive(timestamp: Dayjs) { + const now: Dayjs = dayjs(); + // Block multiple calling, if multiple pong event occur. + // It the duration is less than interval, through ping. + if ( + now.diff(timestamp) > this._heartbeatInterval - 1000 && + !this._connectionClosed + ) { + // Skip ping when client is connecting. + // https://github.com/websockets/ws/blob/7.2.1/lib/websocket.js#L289 + if (this._client && this._client.readyState !== WS.CONNECTING) { + this._pongWaiting = true; + this._client.ping(""); + setTimeout(() => { + if (this._pongWaiting) { + this._pongWaiting = false; + this._reconnect(); + } + }, 10000); + } + } + } +} + +/** + * Parser + * This class provides parser for websocket message. + */ +export class Parser extends EventEmitter { + /** + * @param message Message body of websocket. + * @param channelID Parse only messages which has same channelID. + */ + public parse(data: WS.Data, isBinary: boolean, channelID: string) { + const message = isBinary ? data : data.toString(); + if (typeof message !== "string") { + this.emit("heartbeat", {}); + return; + } + + if (message === "") { + this.emit("heartbeat", {}); + return; + } + + let obj: { + type: string; + body: { + id: string; + type: string; + body: any; + }; + }; + let body: { + id: string; + type: string; + body: any; + }; + + try { + obj = JSON.parse(message); + if (obj.type !== "channel") { + return; + } + if (!obj.body) { + return; + } + body = obj.body; + if (body.id !== channelID) { + return; + } + } catch (err) { + this.emit( + "error", + new Error( + `Error parsing websocket reply: ${message}, error message: ${err}`, + ), + ); + return; + } + + switch (body.type) { + case "note": + this.emit("update", body.body as MisskeyAPI.Entity.Note); + break; + case "notification": + this.emit("notification", body.body as MisskeyAPI.Entity.Notification); + break; + case "mention": { + const note = body.body as MisskeyAPI.Entity.Note; + if (note.visibility === "specified") { + this.emit("conversation", note); + } + break; + } + // When renote and followed event, the same notification will be received. + case "renote": + case "followed": + case "follow": + case "unfollow": + case "receiveFollowRequest": + case "meUpdated": + case "readAllNotifications": + case "readAllUnreadSpecifiedNotes": + case "readAllAntennas": + case "readAllUnreadMentions": + case "unreadNotification": + // Ignore these events + break; + default: + this.emit( + "error", + new Error(`Unknown event has received: ${JSON.stringify(body)}`), + ); + break; + } + } +} diff --git a/packages/megalodon/src/notification.ts b/packages/megalodon/src/notification.ts new file mode 100644 index 0000000000..84cd23e40d --- /dev/null +++ b/packages/megalodon/src/notification.ts @@ -0,0 +1,14 @@ +import Entity from "./entity"; + +namespace NotificationType { + export const Follow: Entity.NotificationType = "follow"; + export const Favourite: Entity.NotificationType = "favourite"; + export const Reblog: Entity.NotificationType = "reblog"; + export const Mention: Entity.NotificationType = "mention"; + export const Reaction: Entity.NotificationType = "reaction"; + export const FollowRequest: Entity.NotificationType = "follow_request"; + export const Status: Entity.NotificationType = "status"; + export const Poll: Entity.NotificationType = "poll"; +} + +export default NotificationType; diff --git a/packages/megalodon/src/oauth.ts b/packages/megalodon/src/oauth.ts new file mode 100644 index 0000000000..f0df721f0a --- /dev/null +++ b/packages/megalodon/src/oauth.ts @@ -0,0 +1,123 @@ +/** + * OAuth + * Response data when oauth request. + **/ +namespace OAuth { + export type AppDataFromServer = { + id: string; + name: string; + website: string | null; + redirect_uri: string; + client_id: string; + client_secret: string; + }; + + export type TokenDataFromServer = { + access_token: string; + token_type: string; + scope: string; + created_at: number; + expires_in: number | null; + refresh_token: string | null; + }; + + export class AppData { + public url: string | null; + public session_token: string | null; + constructor( + public id: string, + public name: string, + public website: string | null, + public redirect_uri: string, + public client_id: string, + public client_secret: string, + ) { + this.url = null; + this.session_token = null; + } + + /** + * Serialize raw application data from server + * @param raw from server + */ + static from(raw: AppDataFromServer) { + return new this( + raw.id, + raw.name, + raw.website, + raw.redirect_uri, + raw.client_id, + raw.client_secret, + ); + } + + get redirectUri() { + return this.redirect_uri; + } + get clientId() { + return this.client_id; + } + get clientSecret() { + return this.client_secret; + } + } + + export class TokenData { + public _scope: string; + constructor( + public access_token: string, + public token_type: string, + scope: string, + public created_at: number, + public expires_in: number | null = null, + public refresh_token: string | null = null, + ) { + this._scope = scope; + } + + /** + * Serialize raw token data from server + * @param raw from server + */ + static from(raw: TokenDataFromServer) { + return new this( + raw.access_token, + raw.token_type, + raw.scope, + raw.created_at, + raw.expires_in, + raw.refresh_token, + ); + } + + /** + * OAuth Aceess Token + */ + get accessToken() { + return this.access_token; + } + get tokenType() { + return this.token_type; + } + get scope() { + return this._scope; + } + /** + * Application ID + */ + get createdAt() { + return this.created_at; + } + get expiresIn() { + return this.expires_in; + } + /** + * OAuth Refresh Token + */ + get refreshToken() { + return this.refresh_token; + } + } +} + +export default OAuth; diff --git a/packages/megalodon/src/parser.ts b/packages/megalodon/src/parser.ts new file mode 100644 index 0000000000..2ddf2ac2e6 --- /dev/null +++ b/packages/megalodon/src/parser.ts @@ -0,0 +1,94 @@ +import { EventEmitter } from "events"; +import Entity from "./entity"; + +/** + * Parser + * Parse response data in streaming. + **/ +export class Parser extends EventEmitter { + private message: string; + + constructor() { + super(); + this.message = ""; + } + + public parse(chunk: string) { + // skip heartbeats + if (chunk === ":thump\n") { + this.emit("heartbeat", {}); + return; + } + + this.message += chunk; + chunk = this.message; + + const size: number = chunk.length; + let start = 0; + let offset = 0; + let curr: string | undefined; + let next: string | undefined; + + while (offset < size) { + curr = chunk[offset]; + next = chunk[offset + 1]; + + if (curr === "\n" && next === "\n") { + const piece: string = chunk.slice(start, offset); + + offset += 2; + start = offset; + + if (!piece.length) continue; // empty object + + const root: Array = piece.split("\n"); + + // should never happen, as long as mastodon doesn't change API messages + if (root.length !== 2) continue; + + // remove event and data markers + const event: string = root[0].substr(7); + const data: string = root[1].substr(6); + + let jsonObj = {}; + try { + jsonObj = JSON.parse(data); + } catch (err) { + // delete event does not have json object + if (event !== "delete") { + this.emit( + "error", + new Error( + `Error parsing API reply: '${piece}', error message: '${err}'`, + ), + ); + continue; + } + } + switch (event) { + case "update": + this.emit("update", jsonObj as Entity.Status); + break; + case "notification": + this.emit("notification", jsonObj as Entity.Notification); + break; + case "conversation": + this.emit("conversation", jsonObj as Entity.Conversation); + break; + case "delete": + // When delete, data is an ID of the deleted status + this.emit("delete", data); + break; + default: + this.emit( + "error", + new Error(`Unknown event has received: ${event}`), + ); + continue; + } + } + offset++; + } + this.message = chunk.slice(start, size); + } +} diff --git a/packages/megalodon/src/proxy_config.ts b/packages/megalodon/src/proxy_config.ts new file mode 100644 index 0000000000..fadbcf084e --- /dev/null +++ b/packages/megalodon/src/proxy_config.ts @@ -0,0 +1,92 @@ +import { HttpsProxyAgent, HttpsProxyAgentOptions } from "https-proxy-agent"; +import { SocksProxyAgent, SocksProxyAgentOptions } from "socks-proxy-agent"; + +export type ProxyConfig = { + host: string; + port: number; + auth?: { + username: string; + password: string; + }; + protocol: + | "http" + | "https" + | "socks4" + | "socks4a" + | "socks5" + | "socks5h" + | "socks"; +}; + +class ProxyProtocolError extends Error {} + +const proxyAgent = ( + proxyConfig: ProxyConfig, +): HttpsProxyAgent | SocksProxyAgent => { + switch (proxyConfig.protocol) { + case "http": { + let options: HttpsProxyAgentOptions = { + host: proxyConfig.host, + port: proxyConfig.port, + secureProxy: false, + }; + if (proxyConfig.auth) { + options = Object.assign(options, { + auth: `${proxyConfig.auth.username}:${proxyConfig.auth.password}`, + }); + } + const httpsAgent = new HttpsProxyAgent(options); + return httpsAgent; + } + case "https": { + let options: HttpsProxyAgentOptions = { + host: proxyConfig.host, + port: proxyConfig.port, + secureProxy: true, + }; + if (proxyConfig.auth) { + options = Object.assign(options, { + auth: `${proxyConfig.auth.username}:${proxyConfig.auth.password}`, + }); + } + const httpsAgent = new HttpsProxyAgent(options); + return httpsAgent; + } + case "socks4": + case "socks4a": { + let options: SocksProxyAgentOptions = { + type: 4, + hostname: proxyConfig.host, + port: proxyConfig.port, + }; + if (proxyConfig.auth) { + options = Object.assign(options, { + userId: proxyConfig.auth.username, + password: proxyConfig.auth.password, + }); + } + const socksAgent = new SocksProxyAgent(options); + return socksAgent; + } + case "socks5": + case "socks5h": + case "socks": { + let options: SocksProxyAgentOptions = { + type: 5, + hostname: proxyConfig.host, + port: proxyConfig.port, + }; + if (proxyConfig.auth) { + options = Object.assign(options, { + userId: proxyConfig.auth.username, + password: proxyConfig.auth.password, + }); + } + const socksAgent = new SocksProxyAgent(options); + return socksAgent; + } + default: + throw new ProxyProtocolError("protocol is not accepted"); + } +}; +export default proxyAgent; diff --git a/packages/megalodon/src/response.ts b/packages/megalodon/src/response.ts new file mode 100644 index 0000000000..13fd8ab574 --- /dev/null +++ b/packages/megalodon/src/response.ts @@ -0,0 +1,8 @@ +type Response = { + data: T; + status: number; + statusText: string; + headers: any; +}; + +export default Response; diff --git a/packages/megalodon/test/integration/megalodon.spec.ts b/packages/megalodon/test/integration/megalodon.spec.ts new file mode 100644 index 0000000000..8964535509 --- /dev/null +++ b/packages/megalodon/test/integration/megalodon.spec.ts @@ -0,0 +1,27 @@ +import { detector } from '../../src/index' + +describe('detector', () => { + describe('mastodon', () => { + const url = 'https://fedibird.com' + it('should be mastodon', async () => { + const mastodon = await detector(url) + expect(mastodon).toEqual('mastodon') + }) + }) + + describe('pleroma', () => { + const url = 'https://pleroma.soykaf.com' + it('should be pleroma', async () => { + const pleroma = await detector(url) + expect(pleroma).toEqual('pleroma') + }) + }) + + describe('misskey', () => { + const url = 'https://misskey.io' + it('should be misskey', async () => { + const misskey = await detector(url) + expect(misskey).toEqual('misskey') + }) + }) +}) diff --git a/packages/megalodon/test/integration/misskey.spec.ts b/packages/megalodon/test/integration/misskey.spec.ts new file mode 100644 index 0000000000..0ec1288428 --- /dev/null +++ b/packages/megalodon/test/integration/misskey.spec.ts @@ -0,0 +1,204 @@ +import MisskeyEntity from '@/misskey/entity' +import MisskeyNotificationType from '@/misskey/notification' +import Misskey from '@/misskey' +import MegalodonNotificationType from '@/notification' +import axios, { AxiosResponse } from 'axios' + +jest.mock('axios') + +const user: MisskeyEntity.User = { + id: '1', + name: 'test_user', + username: 'TestUser', + host: 'misskey.io', + avatarUrl: 'https://example.com/icon.png', + avatarColor: '#000000', + emojis: [] +} + +const note: MisskeyEntity.Note = { + id: '1', + createdAt: '2021-02-01T01:49:29', + userId: '1', + user: user, + text: 'hogehoge', + cw: null, + visibility: 'public', + renoteCount: 0, + repliesCount: 0, + reactions: {}, + emojis: [], + fileIds: [], + files: [], + replyId: null, + renoteId: null +} + +const follow: MisskeyEntity.Notification = { + id: '1', + createdAt: '2021-02-01T01:49:29', + userId: user.id, + user: user, + type: MisskeyNotificationType.Follow +} + +const mention: MisskeyEntity.Notification = { + id: '1', + createdAt: '2021-02-01T01:49:29', + userId: user.id, + user: user, + type: MisskeyNotificationType.Mention, + note: note +} + +const reply: MisskeyEntity.Notification = { + id: '1', + createdAt: '2021-02-01T01:49:29', + userId: user.id, + user: user, + type: MisskeyNotificationType.Reply, + note: note +} + +const renote: MisskeyEntity.Notification = { + id: '1', + createdAt: '2021-02-01T01:49:29', + userId: user.id, + user: user, + type: MisskeyNotificationType.Renote, + note: note +} + +const quote: MisskeyEntity.Notification = { + id: '1', + createdAt: '2021-02-01T01:49:29', + userId: user.id, + user: user, + type: MisskeyNotificationType.Quote, + note: note +} + +const reaction: MisskeyEntity.Notification = { + id: '1', + createdAt: '2021-02-01T01:49:29', + userId: user.id, + user: user, + type: MisskeyNotificationType.Reaction, + note: note, + reaction: '♥' +} + +const pollVote: MisskeyEntity.Notification = { + id: '1', + createdAt: '2021-02-01T01:49:29', + userId: user.id, + user: user, + type: MisskeyNotificationType.PollEnded, + note: note +} + +const receiveFollowRequest: MisskeyEntity.Notification = { + id: '1', + createdAt: '2021-02-01T01:49:29', + userId: user.id, + user: user, + type: MisskeyNotificationType.ReceiveFollowRequest +} + +const followRequestAccepted: MisskeyEntity.Notification = { + id: '1', + createdAt: '2021-02-01T01:49:29', + userId: user.id, + user: user, + type: MisskeyNotificationType.FollowRequestAccepted +} + +const groupInvited: MisskeyEntity.Notification = { + id: '1', + createdAt: '2021-02-01T01:49:29', + userId: user.id, + user: user, + type: MisskeyNotificationType.GroupInvited +} + +;(axios.CancelToken.source as any).mockImplementation(() => { + return { + token: { + throwIfRequested: () => {}, + promise: { + then: () => {}, + catch: () => {} + } + } + } +}) + +describe('getNotifications', () => { + const client = new Misskey('http://localhost', 'sample token') + const cases: Array<{ event: MisskeyEntity.Notification; expected: Entity.NotificationType; title: string }> = [ + { + event: follow, + expected: MegalodonNotificationType.Follow, + title: 'follow' + }, + { + event: mention, + expected: MegalodonNotificationType.Mention, + title: 'mention' + }, + { + event: reply, + expected: MegalodonNotificationType.Mention, + title: 'reply' + }, + { + event: renote, + expected: MegalodonNotificationType.Reblog, + title: 'renote' + }, + { + event: quote, + expected: MegalodonNotificationType.Reblog, + title: 'quote' + }, + { + event: reaction, + expected: MegalodonNotificationType.Reaction, + title: 'reaction' + }, + { + event: pollVote, + expected: MegalodonNotificationType.Poll, + title: 'pollVote' + }, + { + event: receiveFollowRequest, + expected: MegalodonNotificationType.FollowRequest, + title: 'receiveFollowRequest' + }, + { + event: followRequestAccepted, + expected: MegalodonNotificationType.Follow, + title: 'followRequestAccepted' + }, + { + event: groupInvited, + expected: MisskeyNotificationType.GroupInvited, + title: 'groupInvited' + } + ] + cases.forEach(c => { + it(`should be ${c.title} event`, async () => { + const mockResponse: AxiosResponse> = { + data: [c.event], + status: 200, + statusText: '200OK', + headers: {}, + config: {} + } + ;(axios.post as any).mockResolvedValue(mockResponse) + const res = await client.getNotifications() + expect(res.data[0].type).toEqual(c.expected) + }) + }) +}) diff --git a/packages/megalodon/test/unit/misskey/api_client.spec.ts b/packages/megalodon/test/unit/misskey/api_client.spec.ts new file mode 100644 index 0000000000..7cf33b983d --- /dev/null +++ b/packages/megalodon/test/unit/misskey/api_client.spec.ts @@ -0,0 +1,233 @@ +import MisskeyAPI from '@/misskey/api_client' +import MegalodonEntity from '@/entity' +import MisskeyEntity from '@/misskey/entity' +import MegalodonNotificationType from '@/notification' +import MisskeyNotificationType from '@/misskey/notification' + +const user: MisskeyEntity.User = { + id: '1', + name: 'test_user', + username: 'TestUser', + host: 'misskey.io', + avatarUrl: 'https://example.com/icon.png', + avatarColor: '#000000', + emojis: [] +} + +const converter: MisskeyAPI.Converter = new MisskeyAPI.Converter("https://example.com") + +describe('api_client', () => { + describe('notification', () => { + describe('encode', () => { + it('megalodon notification type should be encoded to misskey notification type', () => { + const cases: Array<{ src: MegalodonEntity.NotificationType; dist: MisskeyEntity.NotificationType }> = [ + { + src: MegalodonNotificationType.Follow, + dist: MisskeyNotificationType.Follow + }, + { + src: MegalodonNotificationType.Mention, + dist: MisskeyNotificationType.Reply + }, + { + src: MegalodonNotificationType.Favourite, + dist: MisskeyNotificationType.Reaction + }, + { + src: MegalodonNotificationType.Reaction, + dist: MisskeyNotificationType.Reaction + }, + { + src: MegalodonNotificationType.Reblog, + dist: MisskeyNotificationType.Renote + }, + { + src: MegalodonNotificationType.Poll, + dist: MisskeyNotificationType.PollEnded + }, + { + src: MegalodonNotificationType.FollowRequest, + dist: MisskeyNotificationType.ReceiveFollowRequest + } + ] + cases.forEach(c => { + expect(converter.encodeNotificationType(c.src)).toEqual(c.dist) + }) + }) + }) + describe('decode', () => { + it('misskey notification type should be decoded to megalodon notification type', () => { + const cases: Array<{ src: MisskeyEntity.NotificationType; dist: MegalodonEntity.NotificationType }> = [ + { + src: MisskeyNotificationType.Follow, + dist: MegalodonNotificationType.Follow + }, + { + src: MisskeyNotificationType.Mention, + dist: MegalodonNotificationType.Mention + }, + { + src: MisskeyNotificationType.Reply, + dist: MegalodonNotificationType.Mention + }, + { + src: MisskeyNotificationType.Renote, + dist: MegalodonNotificationType.Reblog + }, + { + src: MisskeyNotificationType.Quote, + dist: MegalodonNotificationType.Reblog + }, + { + src: MisskeyNotificationType.Reaction, + dist: MegalodonNotificationType.Reaction + }, + { + src: MisskeyNotificationType.PollEnded, + dist: MegalodonNotificationType.Poll + }, + { + src: MisskeyNotificationType.ReceiveFollowRequest, + dist: MegalodonNotificationType.FollowRequest + }, + { + src: MisskeyNotificationType.FollowRequestAccepted, + dist: MegalodonNotificationType.Follow + } + ] + cases.forEach(c => { + expect(converter.decodeNotificationType(c.src)).toEqual(c.dist) + }) + }) + }) + }) + describe('reactions', () => { + it('should be mapped', () => { + const misskeyReactions = [ + { + id: '1', + createdAt: '2020-04-21T13:04:13.968Z', + user: { + id: '81u70uwsja', + name: 'h3poteto', + username: 'h3poteto', + host: null, + avatarUrl: 'https://s3.arkjp.net/misskey/thumbnail-63807d97-20ca-40ba-9493-179aa48065c1.png', + avatarColor: 'rgb(146,189,195)', + emojis: [] + }, + type: '❤' + }, + { + id: '2', + createdAt: '2020-04-21T13:04:13.968Z', + user: { + id: '81u70uwsja', + name: 'h3poteto', + username: 'h3poteto', + host: null, + avatarUrl: 'https://s3.arkjp.net/misskey/thumbnail-63807d97-20ca-40ba-9493-179aa48065c1.png', + avatarColor: 'rgb(146,189,195)', + emojis: [] + }, + type: '❤' + }, + { + id: '3', + createdAt: '2020-04-21T13:04:13.968Z', + user: { + id: '81u70uwsja', + name: 'h3poteto', + username: 'h3poteto', + host: null, + avatarUrl: 'https://s3.arkjp.net/misskey/thumbnail-63807d97-20ca-40ba-9493-179aa48065c1.png', + avatarColor: 'rgb(146,189,195)', + emojis: [] + }, + type: '☺' + }, + { + id: '4', + createdAt: '2020-04-21T13:04:13.968Z', + user: { + id: '81u70uwsja', + name: 'h3poteto', + username: 'h3poteto', + host: null, + avatarUrl: 'https://s3.arkjp.net/misskey/thumbnail-63807d97-20ca-40ba-9493-179aa48065c1.png', + avatarColor: 'rgb(146,189,195)', + emojis: [] + }, + type: '❤' + } + ] + + const reactions = converter.reactions(misskeyReactions) + expect(reactions).toEqual([ + { + count: 3, + me: false, + name: '❤' + }, + { + count: 1, + me: false, + name: '☺' + } + ]) + }) + }) + + describe('status', () => { + describe('plain content', () => { + it('should be exported plain content and html content', () => { + const plainContent = 'hoge\nfuga\nfuga' + const content = 'hoge
fuga
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 = '

hoge\nfuga\nfuga

' + const content = '<p>hoge
fuga
fuga<p>' + const note: MisskeyEntity.Note = { + id: '1', + createdAt: '2021-02-01T01:49:29', + userId: '1', + user: user, + text: plainContent, + cw: null, + visibility: 'public', + renoteCount: 0, + repliesCount: 0, + reactions: {}, + emojis: [], + fileIds: [], + files: [], + replyId: null, + renoteId: null + } + const megalodonStatus = converter.note(note, user.host || 'misskey.io') + expect(megalodonStatus.plain_content).toEqual(plainContent) + expect(megalodonStatus.content).toEqual(content) + }) + }) + }) +}) diff --git a/packages/megalodon/test/unit/parser.spec.ts b/packages/megalodon/test/unit/parser.spec.ts new file mode 100644 index 0000000000..5174a647c6 --- /dev/null +++ b/packages/megalodon/test/unit/parser.spec.ts @@ -0,0 +1,152 @@ +import { Parser } from '@/parser' +import Entity from '@/entity' + +const account: Entity.Account = { + id: '1', + username: 'h3poteto', + acct: 'h3poteto@pleroma.io', + display_name: 'h3poteto', + locked: false, + created_at: '2019-03-26T21:30:32', + followers_count: 10, + following_count: 10, + statuses_count: 100, + note: 'engineer', + url: 'https://pleroma.io', + avatar: '', + avatar_static: '', + header: '', + header_static: '', + emojis: [], + moved: null, + fields: [], + bot: false +} + +const status: Entity.Status = { + id: '1', + uri: 'http://example.com', + url: 'http://example.com', + account: account, + in_reply_to_id: null, + in_reply_to_account_id: null, + reblog: null, + content: 'hoge', + plain_content: 'hoge', + created_at: '2019-03-26T21:40:32', + emojis: [], + replies_count: 0, + reblogs_count: 0, + favourites_count: 0, + reblogged: null, + favourited: null, + muted: null, + sensitive: false, + spoiler_text: '', + visibility: 'public', + media_attachments: [], + mentions: [], + tags: [], + card: null, + poll: null, + application: { + name: 'Web' + } as Entity.Application, + language: null, + pinned: null, + reactions: [], + bookmarked: false, + quote: null +} + +const notification: Entity.Notification = { + id: '1', + account: account, + status: status, + type: 'favourite', + created_at: '2019-04-01T17:01:32' +} + +const conversation: Entity.Conversation = { + id: '1', + accounts: [account], + last_status: status, + unread: true +} + +describe('Parser', () => { + let parser: Parser + + beforeEach(() => { + parser = new Parser() + }) + + describe('parse', () => { + describe('message is heartbeat', () => { + const message: string = ':thump\n' + it('should be called', () => { + const spy = jest.fn() + parser.on('heartbeat', spy) + parser.parse(message) + expect(spy).toHaveBeenLastCalledWith({}) + }) + }) + + describe('message is not json', () => { + describe('event is delete', () => { + const message = `event: delete\ndata: 12asdf34\n\n` + it('should be called', () => { + const spy = jest.fn() + parser.once('delete', spy) + parser.parse(message) + expect(spy).toHaveBeenCalledWith('12asdf34') + }) + }) + + describe('event is not delete', () => { + const message = `event: event\ndata: 12asdf34\n\n` + it('should be error', () => { + const error = jest.fn() + const deleted = jest.fn() + parser.once('error', error) + parser.once('delete', deleted) + parser.parse(message) + expect(error).toHaveBeenCalled() + expect(deleted).not.toHaveBeenCalled() + }) + }) + }) + + describe('message is json', () => { + describe('event is update', () => { + const message = `event: update\ndata: ${JSON.stringify(status)}\n\n` + it('should be called', () => { + const spy = jest.fn() + parser.once('update', spy) + parser.parse(message) + expect(spy).toHaveBeenCalledWith(status) + }) + }) + + describe('event is notification', () => { + const message = `event: notification\ndata: ${JSON.stringify(notification)}\n\n` + it('should be called', () => { + const spy = jest.fn() + parser.once('notification', spy) + parser.parse(message) + expect(spy).toHaveBeenCalledWith(notification) + }) + }) + + describe('event is conversation', () => { + const message = `event: conversation\ndata: ${JSON.stringify(conversation)}\n\n` + it('should be called', () => { + const spy = jest.fn() + parser.once('conversation', spy) + parser.parse(message) + expect(spy).toHaveBeenCalledWith(conversation) + }) + }) + }) + }) +}) diff --git a/packages/megalodon/tsconfig.json b/packages/megalodon/tsconfig.json new file mode 100644 index 0000000000..5a9bfbde9a --- /dev/null +++ b/packages/megalodon/tsconfig.json @@ -0,0 +1,64 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "lib": ["es2021", "dom"], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./lib", /* Redirect output structure to the directory. */ + "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + "strictFunctionTypes": true, /* Enable strict checking of function types. */ + "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + "noUnusedLocals": false, /* Report errors on unused locals. */ + "noUnusedParameters": true, /* Report errors on unused parameters. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + "paths": { + "@*": ["src*"], + "~*": ["./*"] + }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + }, + "include": ["./src", "./test"], + "exclude": ["node_modules", "example"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f56048c1bd..42feff5589 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -280,8 +280,8 @@ importers: specifier: 7.0.2 version: 7.0.2(@types/koa@2.13.8)(ejs@3.1.9)(pug@3.0.2) megalodon: - specifier: 8.1.1 - version: 8.1.1 + specifier: workspace:* + version: link:../megalodon meilisearch: specifier: 0.34.1 version: 0.34.1 @@ -950,6 +950,124 @@ importers: specifier: 5.1.3 version: 5.1.3 + packages/megalodon: + dependencies: + '@types/oauth': + specifier: ^0.9.0 + version: 0.9.1 + '@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.10 + 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.0 + 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.7 + '@types/form-data': + specifier: ^2.5.0 + version: 2.5.0 + '@types/jest': + specifier: ^29.4.0 + version: 29.5.6 + '@types/node': + specifier: 18.11.18 + version: 18.11.18 + '@types/object-assign-deep': + specifier: ^0.4.0 + version: 0.4.2 + '@types/parse-link-header': + specifier: ^2.0.0 + version: 2.0.2 + '@types/uuid': + specifier: ^9.0.0 + version: 9.0.3 + '@typescript-eslint/eslint-plugin': + specifier: ^5.49.0 + version: 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@4.9.4) + '@typescript-eslint/parser': + specifier: ^5.49.0 + version: 5.62.0(eslint@8.46.0)(typescript@4.9.4) + eslint: + specifier: ^8.32.0 + version: 8.46.0 + eslint-config-prettier: + specifier: ^8.6.0 + version: 8.9.0(eslint@8.46.0) + eslint-config-standard: + specifier: ^16.0.3 + version: 16.0.3(eslint-plugin-import@2.28.0)(eslint-plugin-node@11.1.0)(eslint-plugin-promise@6.1.1)(eslint@8.46.0) + eslint-plugin-import: + specifier: ^2.27.5 + version: 2.28.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0) + eslint-plugin-node: + specifier: ^11.0.0 + version: 11.1.0(eslint@8.46.0) + eslint-plugin-prettier: + specifier: ^4.2.1 + version: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@2.8.8) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.1.1(eslint@8.46.0) + eslint-plugin-standard: + specifier: ^5.0.0 + version: 5.0.0(eslint@8.46.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.14 + version: 4.17.21 + prettier: + specifier: ^2.8.3 + version: 2.8.8 + ts-jest: + specifier: ^29.0.5 + version: 29.1.1(@babel/core@7.22.10)(jest@29.7.0)(typescript@4.9.4) + typedoc: + specifier: ^0.23.24 + version: 0.23.28(typescript@4.9.4) + packages/sw: optionalDependencies: '@swc/core-android-arm64': @@ -1209,6 +1327,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.22.10): + resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.22.10): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -2197,6 +2325,18 @@ packages: slash: 3.0.0 dev: true + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.5.8 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + dev: true + /@jest/core@27.5.1(ts-node@10.4.0): resolution: {integrity: sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2242,6 +2382,49 @@ packages: - utf-8-validate dev: true + /@jest/core@29.7.0: + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.5.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.8.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.5.8) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /@jest/environment@27.5.1: resolution: {integrity: sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2252,6 +2435,33 @@ packages: jest-mock: 27.5.1 dev: true + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.5.8 + jest-mock: 29.7.0 + dev: true + + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + dev: true + + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/fake-timers@27.5.1: resolution: {integrity: sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2264,6 +2474,18 @@ packages: jest-util: 27.5.1 dev: true + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.5.8 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + /@jest/globals@27.5.1: resolution: {integrity: sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2273,6 +2495,18 @@ packages: expect: 27.5.1 dev: true + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/reporters@27.5.1: resolution: {integrity: sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2311,6 +2545,43 @@ packages: - supports-color dev: true + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.19 + '@types/node': 20.5.8 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-instrument: 6.0.1 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.6 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.1.3 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/schemas@29.6.0: resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2318,6 +2589,13 @@ packages: '@sinclair/typebox': 0.27.8 dev: true + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + /@jest/source-map@27.5.1: resolution: {integrity: sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2327,6 +2605,15 @@ packages: source-map: 0.6.1 dev: true + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + callsites: 3.1.0 + graceful-fs: 4.2.11 + dev: true + /@jest/test-result@27.5.1: resolution: {integrity: sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2337,6 +2624,16 @@ packages: collect-v8-coverage: 1.0.2 dev: true + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.4 + collect-v8-coverage: 1.0.2 + dev: true + /@jest/test-sequencer@27.5.1: resolution: {integrity: sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2349,6 +2646,16 @@ packages: - supports-color dev: true + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + dev: true + /@jest/transform@27.5.1: resolution: {integrity: sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2372,6 +2679,29 @@ packages: - supports-color dev: true + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.22.10 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.19 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.5 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/types@27.5.1: resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2383,6 +2713,18 @@ packages: chalk: 4.1.2 dev: true + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 20.5.8 + '@types/yargs': 17.0.29 + chalk: 4.1.2 + dev: true + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -2902,6 +3244,18 @@ packages: dependencies: type-detect: 4.0.8 + /@sinonjs/commons@3.0.0: + resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.0 + dev: true + /@sinonjs/fake-timers@8.1.0: resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==} dependencies: @@ -3432,6 +3786,10 @@ packages: resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} dev: true + /@types/async-lock@1.4.0: + resolution: {integrity: sha512-2+rYSaWrpdbQG3SA0LmMT6YxWLrI81AqpMlSkw3QtFc2HGDufkweQSn30Eiev7x9LL0oyFrBqk1PXOnB9IEgKg==} + dev: true + /@types/babel__core@7.20.1: resolution: {integrity: sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==} dependencies: @@ -3502,6 +3860,10 @@ packages: '@types/keygrip': 1.0.2 '@types/node': 20.5.8 + /@types/core-js@2.5.7: + resolution: {integrity: sha512-EhO4Lcd2Rs2bZvQwIDMZ1qsaZk8DpdOkQCbKpK0vt7fSjJGXrCA7EPauR/BZ7eJXks1een4FX7JtlhS136fklA==} + dev: true + /@types/disposable-email-domains@1.0.4: resolution: {integrity: sha512-AmKPD8vBZzvey/jeg+YAIH/xJE3D6edOXz+YUooSCcHesGzFyzke83kj1j4d0LUR9nkSHIRklUVdcAMleuWLpg==} dev: false @@ -3561,6 +3923,13 @@ packages: '@types/node': 18.11.18 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/formidable@2.0.6: resolution: {integrity: sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w==} dependencies: @@ -3635,6 +4004,13 @@ packages: pretty-format: 27.5.1 dev: true + /@types/jest@29.5.6: + resolution: {integrity: sha512-/t9NnzkOpXb4Nfvg17ieHE6EeSjDS2SGSpNYfoLbUAeL/EOueU/RSdOWFpfQTXBEM7BguYW1XQ0EbM+6RlIh6w==} + dependencies: + expect: 29.7.0 + pretty-format: 29.6.2 + dev: true + /@types/js-yaml@4.0.5: resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==} dev: true @@ -3884,13 +4260,10 @@ packages: resolution: {integrity: sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==} dependencies: '@types/node': 18.11.18 - dev: true - /@types/oauth@0.9.2: - resolution: {integrity: sha512-Nu3/abQ6yR9VlsCdX3aiGsWFkj6OJvJqDvg/36t8Gwf2mFXdBZXPDN3K+2yfeA6Lo2m1Q12F8Qil9TZ48nWhOQ==} - dependencies: - '@types/node': 20.5.8 - dev: false + /@types/object-assign-deep@0.4.2: + resolution: {integrity: sha512-iF6qYKjYdg/kFg3AEM/msyh1+U4zZW043d2TnCS9fwib00nc8Asj+38LgIpkO/UpfUMRgJ0m/tHATwU2F8Bfow==} + dev: true /@types/offscreencanvas@2019.3.0: resolution: {integrity: sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==} @@ -3900,6 +4273,10 @@ packages: resolution: {integrity: sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==} dev: false + /@types/parse-link-header@2.0.2: + resolution: {integrity: sha512-RKU5SIF0oyM2ZI0ubw66FkM/0RJUv/r84I7vJcXkcICcfeOpd1WXfpcqkFJPaWli5z3YdxMsfWojyU5uofT6sA==} + dev: true + /@types/picomatch@2.3.0: resolution: {integrity: sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==} dev: true @@ -4104,6 +4481,12 @@ packages: '@types/yargs-parser': 21.0.0 dev: true + /@types/yargs@17.0.29: + resolution: {integrity: sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==} + dependencies: + '@types/yargs-parser': 21.0.0 + dev: true + /@types/yauzl@2.10.0: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true @@ -4112,6 +4495,34 @@ packages: dev: true optional: true + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@4.9.4): + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} + 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: + '@eslint-community/regexpp': 4.6.2 + '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@4.9.4) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@8.46.0)(typescript@4.9.4) + '@typescript-eslint/utils': 5.62.0(eslint@8.46.0)(typescript@4.9.4) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.46.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare-lite: 1.4.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@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@5.2.2): resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4170,6 +4581,26 @@ packages: - supports-color dev: true + /@typescript-eslint/parser@5.62.0(eslint@8.46.0)(typescript@4.9.4): + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + 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.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.4) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.46.0 + typescript: 4.9.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@5.62.0(eslint@8.46.0)(typescript@5.2.2): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4227,6 +4658,26 @@ packages: '@typescript-eslint/visitor-keys': 6.3.0 dev: true + /@typescript-eslint/type-utils@5.62.0(eslint@8.46.0)(typescript@4.9.4): + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.4) + '@typescript-eslint/utils': 5.62.0(eslint@8.46.0)(typescript@4.9.4) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.46.0 + tsutils: 3.21.0(typescript@4.9.4) + typescript: 4.9.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/type-utils@5.62.0(eslint@8.46.0)(typescript@5.2.2): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4277,6 +4728,27 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true + /@typescript-eslint/typescript-estree@5.62.0(typescript@4.9.4): + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.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@5.62.0(typescript@5.2.2): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4319,6 +4791,26 @@ packages: - supports-color dev: true + /@typescript-eslint/utils@5.62.0(eslint@8.46.0)(typescript@4.9.4): + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) + '@types/json-schema': 7.0.12 + '@types/semver': 7.5.0 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.4) + eslint: 8.46.0 + eslint-scope: 5.1.1 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/utils@5.62.0(eslint@8.46.0)(typescript@5.2.2): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4798,6 +5290,10 @@ packages: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} + /ansi-sequence-parser@1.1.1: + resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} + dev: true + /ansi-styles@2.2.1: resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} engines: {node: '>=0.10.0'} @@ -5169,6 +5665,10 @@ packages: stream-exhaust: 1.0.2 dev: true + /async-lock@1.4.0: + resolution: {integrity: sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==} + dev: false + /async-settle@1.0.0: resolution: {integrity: sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==} engines: {node: '>= 0.10'} @@ -5325,8 +5825,8 @@ packages: - debug dev: true - /axios@1.4.0: - resolution: {integrity: sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==} + /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 @@ -5335,8 +5835,8 @@ packages: - debug dev: false - /axios@1.5.1: - resolution: {integrity: sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==} + /axios@1.4.0: + resolution: {integrity: sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==} dependencies: follow-redirects: 1.15.2(debug@4.3.4) form-data: 4.0.0 @@ -5368,6 +5868,24 @@ packages: - supports-color dev: true + /babel-jest@29.7.0(@babel/core@7.22.10): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.22.10 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.1 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.22.10) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /babel-plugin-istanbul@6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} @@ -5391,6 +5909,16 @@ packages: '@types/babel__traverse': 7.20.1 dev: true + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.22.5 + '@babel/types': 7.22.10 + '@types/babel__core': 7.20.1 + '@types/babel__traverse': 7.20.1 + dev: true + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.22.10): resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} peerDependencies: @@ -5422,6 +5950,17 @@ packages: babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.10) dev: true + /babel-preset-jest@29.6.3(@babel/core@7.22.10): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.10 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.10) + dev: true + /babel-walk@3.0.0-canary-5: resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} engines: {node: '>= 10.0.0'} @@ -6711,6 +7250,10 @@ packages: /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + /convert-to-spaces@2.0.1: resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6769,6 +7312,25 @@ packages: readable-stream: 3.6.2 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-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -7172,6 +7734,15 @@ packages: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dev: true + /deep-email-validator@0.1.21: resolution: {integrity: sha512-DBAmMzbr+MAubXQ+TS9tZuPwLcdKscb8YzKZiwoLqF3NmaeEgXvSSHhZ0EXOFeKFE2FNWC4mNXCyiQ/JdFXUwg==} dependencies: @@ -7352,6 +7923,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -7517,6 +8093,11 @@ packages: /electron-to-chromium@1.4.488: resolution: {integrity: sha512-Dv4sTjiW7t/UWGL+H8ZkgIjtUAVZDgb/PwGWvMsCT7jipzUV/u5skbLXPFKb6iV0tiddVi/bcS2/kUrczeWgIQ==} + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + dev: true + /emittery@0.8.1: resolution: {integrity: sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==} engines: {node: '>=10'} @@ -7814,6 +8395,20 @@ packages: eslint: 8.46.0 dev: true + /eslint-config-standard@16.0.3(eslint-plugin-import@2.28.0)(eslint-plugin-node@11.1.0)(eslint-plugin-promise@6.1.1)(eslint@8.46.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.46.0 + eslint-plugin-import: 2.28.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0) + eslint-plugin-node: 11.1.0(eslint@8.46.0) + eslint-plugin-promise: 6.1.1(eslint@8.46.0) + dev: true + /eslint-formatter-pretty@4.1.0: resolution: {integrity: sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==} engines: {node: '>=10'} @@ -7859,7 +8454,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.2.2) + '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@4.9.4) debug: 3.2.7(supports-color@8.1.1) eslint: 8.46.0 eslint-import-resolver-node: 0.3.9 @@ -7907,6 +8502,17 @@ packages: eslint: 8.46.0 dev: true + /eslint-plugin-es@3.0.1(eslint@8.46.0): + resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + eslint: 8.46.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + dev: true + /eslint-plugin-es@4.1.0(eslint@8.46.0): resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} engines: {node: '>=8.10.0'} @@ -7955,7 +8561,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.2.2) + '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@4.9.4) array-includes: 3.1.6 array.prototype.findlastindex: 1.2.2 array.prototype.flat: 1.3.1 @@ -8095,6 +8701,38 @@ packages: semver: 7.5.4 dev: true + /eslint-plugin-node@11.1.0(eslint@8.46.0): + resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=5.16.0' + dependencies: + eslint: 8.46.0 + eslint-plugin-es: 3.0.1(eslint@8.46.0) + eslint-utils: 2.1.0 + ignore: 5.2.4 + minimatch: 3.1.2 + resolve: 1.22.4 + semver: 6.3.1 + dev: true + + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.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.46.0 + eslint-config-prettier: 8.9.0(eslint@8.46.0) + prettier: 2.8.8 + prettier-linter-helpers: 1.0.0 + dev: true + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.0.3): resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} engines: {node: '>=12.0.0'} @@ -8151,6 +8789,15 @@ packages: eslint: 8.46.0 dev: true + /eslint-plugin-standard@5.0.0(eslint@8.46.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.46.0 + dev: true + /eslint-plugin-tsdoc@0.2.17: resolution: {integrity: sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA==} dependencies: @@ -8581,6 +9228,17 @@ packages: jest-message-util: 27.5.1 dev: true + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + /exponential-backoff@3.1.1: resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} dev: false @@ -9039,7 +9697,6 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: false /formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} @@ -10003,16 +10660,6 @@ packages: - supports-color dev: false - /https-proxy-agent@7.0.2: - resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - dev: false - /human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} @@ -10772,6 +11419,19 @@ packages: - supports-color dev: true + /istanbul-lib-instrument@6.0.1: + resolution: {integrity: sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.22.10 + '@babel/parser': 7.22.10 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} @@ -10836,6 +11496,15 @@ packages: throat: 6.0.2 dev: true + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + dev: true + /jest-circus@27.5.1: resolution: {integrity: sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -10863,6 +11532,35 @@ packages: - supports-color dev: true + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.5.8 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.1 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.0.4 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-cli@27.5.1(ts-node@10.4.0): resolution: {integrity: sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -10893,6 +11591,34 @@ packages: - utf-8-validate 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.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest-config@27.5.1(ts-node@10.4.0): resolution: {integrity: sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -10934,6 +11660,86 @@ packages: - utf-8-validate 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.10 + '@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.10) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + 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.5.8): + 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.10 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.5.8 + babel-jest: 29.7.0(@babel/core@7.22.10) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + 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-diff@27.5.1: resolution: {integrity: sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -10954,6 +11760,16 @@ packages: pretty-format: 29.6.2 dev: true + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + /jest-docblock@27.5.1: resolution: {integrity: sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -10961,6 +11777,13 @@ packages: detect-newline: 3.1.0 dev: true + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + dev: true + /jest-each@27.5.1: resolution: {integrity: sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -10972,6 +11795,17 @@ packages: pretty-format: 27.5.1 dev: true + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + dev: true + /jest-environment-jsdom@27.5.1: resolution: {integrity: sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11002,6 +11836,18 @@ packages: jest-util: 27.5.1 dev: true + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.5.8 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + /jest-fetch-mock@3.0.3: resolution: {integrity: sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==} dependencies: @@ -11021,6 +11867,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /jest-haste-map@27.5.1: resolution: {integrity: sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11041,6 +11892,25 @@ packages: fsevents: 2.3.2 dev: true + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.6 + '@types/node': 20.5.8 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /jest-jasmine2@27.5.1: resolution: {integrity: sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11074,6 +11944,14 @@ packages: pretty-format: 27.5.1 dev: true + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + /jest-matcher-utils@27.5.1: resolution: {integrity: sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11084,6 +11962,16 @@ packages: pretty-format: 27.5.1 dev: true + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + /jest-message-util@27.5.1: resolution: {integrity: sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11099,6 +11987,21 @@ packages: stack-utils: 2.0.6 dev: true + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.22.10 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + /jest-mock@27.5.1: resolution: {integrity: sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11107,6 +12010,15 @@ packages: '@types/node': 20.5.8 dev: true + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.5.8 + jest-util: 29.7.0 + dev: true + /jest-pnp-resolver@1.2.3(jest-resolve@27.5.1): resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} @@ -11119,11 +12031,28 @@ packages: jest-resolve: 27.5.1 dev: true + /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 + dev: true + /jest-regex-util@27.5.1: resolution: {integrity: sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dev: true + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /jest-resolve-dependencies@27.5.1: resolution: {integrity: sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11135,6 +12064,16 @@ packages: - supports-color dev: true + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /jest-resolve@27.5.1: resolution: {integrity: sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11151,6 +12090,21 @@ packages: slash: 3.0.0 dev: true + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.4 + resolve.exports: 2.0.2 + slash: 3.0.0 + dev: true + /jest-runner@27.5.1: resolution: {integrity: sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11183,6 +12137,35 @@ packages: - utf-8-validate dev: true + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.5.8 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: true + /jest-runtime@27.5.1: resolution: {integrity: sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11213,6 +12196,36 @@ packages: - supports-color dev: true + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.5.8 + chalk: 4.1.2 + cjs-module-lexer: 1.2.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /jest-serializer@27.5.1: resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11251,6 +12264,34 @@ packages: - supports-color dev: true + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.22.10 + '@babel/generator': 7.22.10 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.10) + '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.22.10) + '@babel/types': 7.22.10 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.10) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /jest-util@27.5.1: resolution: {integrity: sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11263,6 +12304,18 @@ packages: picomatch: 2.3.1 dev: true + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.5.8 + chalk: 4.1.2 + ci-info: 3.8.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: true + /jest-validate@27.5.1: resolution: {integrity: sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11275,6 +12328,18 @@ packages: pretty-format: 27.5.1 dev: true + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + dev: true + /jest-watcher@27.5.1: resolution: {integrity: sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11288,6 +12353,20 @@ packages: string-length: 4.0.2 dev: true + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.5.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + dev: true + /jest-websocket-mock@2.2.1(mock-socket@9.0.8): resolution: {integrity: sha512-fhsGLXrPfs06PhHoxqOSA9yZ6Rb4qYrf4Wcm7/nfRzjlrf1gIeuhYUkzMRjjE0TMQ37SwkmeLanwrZY4ZaNp8g==} peerDependencies: @@ -11306,6 +12385,16 @@ packages: supports-color: 8.1.1 dev: true + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 20.5.8 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + /jest@27.4.5(ts-node@10.4.0): resolution: {integrity: sha512-uT5MiVN3Jppt314kidCk47MYIRilJjA/l2mxwiuzzxGUeJIvA8/pDaJOAX5KWvjAo7SCydcW0/4WEtgbLMiJkg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11327,6 +12416,27 @@ packages: - utf-8-validate 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 + /jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} dev: true @@ -11547,6 +12657,10 @@ packages: semver: 7.5.4 dev: true + /jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + /jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} optionalDependencies: @@ -12241,6 +13355,10 @@ packages: engines: {node: '>=12'} dev: false + /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'} @@ -12350,6 +13468,12 @@ packages: object-visit: 1.0.1 dev: true + /marked@4.3.0: + resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} + engines: {node: '>= 12'} + hasBin: true + dev: true + /matchdep@2.0.0: resolution: {integrity: sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA==} engines: {node: '>= 0.10.0'} @@ -12405,30 +13529,6 @@ packages: engines: {node: '>= 0.6'} dev: false - /megalodon@8.1.1: - resolution: {integrity: sha512-K7YjGmRbNkJao2E0hadJCW3IDloufVPUbYA/3+RFDFZvZO5v1MBz3rU4OixIgrHHY74PVTkSU8YHzyv7KA4rhA==} - engines: {node: '>=15.0.0'} - dependencies: - '@types/oauth': 0.9.2 - '@types/ws': 8.5.5 - axios: 1.5.1 - dayjs: 1.11.10 - form-data: 4.0.0 - https-proxy-agent: 7.0.2 - oauth: 0.10.0 - object-assign-deep: 0.4.0 - parse-link-header: 2.0.0 - socks-proxy-agent: 8.0.2 - typescript: 5.2.2 - uuid: 9.0.1 - ws: 8.14.2 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - dev: false - /meilisearch@0.34.1: resolution: {integrity: sha512-7mrLp88JfrbvhAMhOjNPzHGd2iCLHgzNhkveMxppMOToMLQw4Ygof4ksQ9uFi7SKq3UwEhIoMoFT1rUHLD3vWQ==} dependencies: @@ -12580,6 +13680,13 @@ packages: dependencies: brace-expansion: 2.0.1 + /minimatch@7.4.6: + resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimatch@9.0.1: resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} engines: {node: '>=16 || 14 >=14.17'} @@ -14270,6 +15377,15 @@ packages: react-is: 18.2.0 dev: true + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + /pretty-hrtime@1.0.3: resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} engines: {node: '>= 0.8'} @@ -14495,6 +15611,10 @@ packages: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} + /pure-rand@6.0.4: + resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} + dev: true + /pureimage@0.4.8: resolution: {integrity: sha512-/yNBs67VB4moPB7tqfupxFhYYaSlpnBpDb5995B0FP+vTqKwR2KD2uEIvch9NmpqUpVnsXcDOsFgmuGTgLX/Lg==} engines: {node: '>=14.19.0'} @@ -15033,6 +16153,11 @@ packages: engines: {node: '>=10'} dev: true + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + dev: true + /resolve@1.19.0: resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} dependencies: @@ -15383,6 +16508,15 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + /shiki@0.14.5: + resolution: {integrity: sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==} + dependencies: + ansi-sequence-parser: 1.1.1 + 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: @@ -15503,17 +16637,6 @@ packages: - supports-color dev: false - /socks-proxy-agent@8.0.2: - resolution: {integrity: sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) - socks: 2.7.1 - transitivePeerDependencies: - - supports-color - dev: false - /socks@2.7.1: resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} @@ -15564,6 +16687,13 @@ packages: decode-uri-component: 0.2.2 dev: true + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: @@ -16500,6 +17630,40 @@ packages: yargs-parser: 20.2.9 dev: true + /ts-jest@29.1.1(@babel/core@7.22.10)(jest@29.7.0)(typescript@4.9.4): + resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} + 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 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.22.10 + 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-loader@9.4.4(typescript@5.1.6)(webpack@5.88.2): resolution: {integrity: sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==} engines: {node: '>=12.0.0'} @@ -16633,6 +17797,16 @@ packages: engines: {node: '>=0.6.x'} dev: false + /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 + /tsutils@3.21.0(typescript@5.2.2): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -16755,6 +17929,20 @@ packages: /typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + /typedoc@0.23.28(typescript@4.9.4): + resolution: {integrity: sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==} + engines: {node: '>= 14.14'} + hasBin: true + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x + dependencies: + lunr: 2.3.9 + marked: 4.3.0 + minimatch: 7.4.6 + shiki: 0.14.5 + typescript: 4.9.4 + dev: true + /typeorm@0.3.17(ioredis@5.3.2)(pg@8.11.3)(ts-node@10.9.1): resolution: {integrity: sha512-UDjUEwIQalO9tWw9O2A4GU+sT3oyoUXheHJy4ft+RFdnRdQctdQ34L9SqE2p7LdwzafHx1maxT+bqXON+Qnmig==} engines: {node: '>= 12.9.0'} @@ -16835,6 +18023,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'} @@ -16856,6 +18049,7 @@ packages: resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} engines: {node: '>=14.17'} hasBin: true + dev: true /ulid@2.3.0: resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} @@ -17086,11 +18280,6 @@ packages: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} hasBin: true - /uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - dev: false - /v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -17103,6 +18292,15 @@ packages: source-map: 0.7.4 dev: true + /v8-to-istanbul@9.1.3: + resolution: {integrity: sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + '@types/istanbul-lib-coverage': 2.0.4 + convert-source-map: 2.0.0 + dev: true + /v8flags@3.2.0: resolution: {integrity: sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==} engines: {node: '>= 0.10'} @@ -17256,6 +18454,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-draggable-plus@0.2.6(@types/sortablejs@1.15.1): resolution: {integrity: sha512-d+0omKIBIfLiJFggc6H4ePRaifbX+33+OiCMsxn8rG59yWXlJGrobexxgXetnSo/1NLTd0TkYZKNc4CA6iwJZw==} peerDependencies: @@ -17624,6 +18830,14 @@ packages: typedarray-to-buffer: 3.1.5 dev: true + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + dev: true + /write-file-atomic@5.0.1: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -17645,6 +18859,19 @@ packages: optional: true 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.13.0: resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} engines: {node: '>=10.0.0'} @@ -17658,19 +18885,6 @@ packages: optional: true dev: true - /ws@8.14.2: - resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: false - /xev@3.0.2: resolution: {integrity: sha512-8kxuH95iMXzHZj+fwqfA4UrPcYOy6bGIgfWzo9Ji23JoEc30ge/Z++Ubkiuy8c0+M64nXmmxrmJ7C8wnuBhluw==} dev: false diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c70c008eaf..fd5cd65e6d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,3 +4,4 @@ packages: - 'packages/client' - 'packages/sw' - 'packages/firefish-js' + - 'packages/megalodon' diff --git a/scripts/clean-all.js b/scripts/clean-all.js index 7beed3fe26..e3394f4098 100644 --- a/scripts/clean-all.js +++ b/scripts/clean-all.js @@ -46,6 +46,14 @@ const { join } = require("node:path"); recursive: true, force: true, }); + fs.rmSync(join(__dirname, "/../packages/megalodon/lib"), { + recursive: true, + force: true, + }); + fs.rmSync(join(__dirname, "/../packages/megalodon/node_modules"), { + recursive: true, + force: true, + }); fs.rmSync(join(__dirname, "/../built"), { recursive: true, force: true }); fs.rmSync(join(__dirname, "/../node_modules"), { diff --git a/scripts/clean.js b/scripts/clean.js index e4ae085b4c..455c436285 100644 --- a/scripts/clean.js +++ b/scripts/clean.js @@ -23,5 +23,9 @@ const { join } = require("node:path"); recursive: true, force: true, }); + fs.rmSync(join(__dirname, "/../packages/megalodon/lib"), { + recursive: true, + force: true, + }); fs.rmSync(join(__dirname, "/../built"), { recursive: true, force: true }); })();