diff --git a/.gitignore b/.gitignore index 3a667851c..29377540d 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ packages/backend/assets/sounds/None.mp3 !packages/backend/src/db +packages/megalodon/lib +packages/megalodon/.idea + # blender backups *.blend1 *.blend2 diff --git a/package.json b/package.json index db96ae1ca..ec899739f 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "seedrandom": "^3.0.5" }, "devDependencies": { + "@types/node": "18.11.18", "@types/gulp": "4.0.10", "@types/gulp-rename": "2.0.1", "chalk": "4.1.2", diff --git a/packages/backend/assets/transparent.png b/packages/backend/assets/transparent.png new file mode 100644 index 000000000..240ca4f8d Binary files /dev/null and b/packages/backend/assets/transparent.png differ diff --git a/packages/backend/native-utils/src/mastodon_api.rs b/packages/backend/native-utils/src/mastodon_api.rs index 3016acd54..6c27d3495 100644 --- a/packages/backend/native-utils/src/mastodon_api.rs +++ b/packages/backend/native-utils/src/mastodon_api.rs @@ -13,7 +13,6 @@ pub enum IdConvertType { #[napi] pub fn convert_id(in_id: String, id_convert_type: IdConvertType) -> napi::Result { - println!("converting id: {}", in_id); use IdConvertType::*; match id_convert_type { MastodonId => { diff --git a/packages/backend/package.json b/packages/backend/package.json index 6f6344102..060064489 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -28,7 +28,7 @@ "@bull-board/api": "5.2.0", "@bull-board/koa": "5.2.0", "@bull-board/ui": "5.2.0", - "@calckey/megalodon": "5.2.0", + "megalodon": "workspace:*", "@discordapp/twemoji": "14.1.2", "@elastic/elasticsearch": "7.17.0", "@koa/cors": "3.4.3", diff --git a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts index df85f2162..753f73ead 100644 --- a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts +++ b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts @@ -1,5 +1,5 @@ import Router from "@koa/router"; -import megalodon, { MegalodonInterface } from "@calckey/megalodon"; +import megalodon, { MegalodonInterface } from "megalodon"; import { apiAuthMastodon } from "./endpoints/auth.js"; import { apiAccountMastodon } from "./endpoints/account.js"; import { apiStatusMastodon } from "./endpoints/status.js"; @@ -18,11 +18,7 @@ export function getClient( const accessTokenArr = authorization?.split(" ") ?? [null]; const accessToken = accessTokenArr[accessTokenArr.length - 1]; const generator = (megalodon as any).default; - const client = generator( - "misskey", - BASE_URL, - accessToken, - ) as MegalodonInterface; + const client = generator(BASE_URL, accessToken) as MegalodonInterface; return client; } diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index 825d0f518..37c6283a1 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -1,4 +1,4 @@ -import { Entity } from "@calckey/megalodon"; +import { Entity } from "megalodon"; import { convertId, IdType } from "../index.js"; function simpleConvert(data: any) { @@ -21,6 +21,9 @@ export function convertFilter(filter: Entity.Filter) { export function convertList(list: Entity.List) { return simpleConvert(list); } +export function convertFeaturedTag(tag: Entity.FeaturedTag) { + return simpleConvert(tag); +} export function convertNotification(notification: Entity.Notification) { notification.account = convertAccount(notification.account); diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index deb5dac30..3fb1c9cbe 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -7,6 +7,7 @@ import { argsToBools, convertTimelinesArgsId, limitToInt } from "./timeline.js"; import { convertId, IdType } from "../../index.js"; import { convertAccount, + convertFeaturedTag, convertList, convertRelationship, convertStatus, @@ -42,8 +43,8 @@ export function apiAccountMastodon(router: Router): void { acct.url = `${BASE_URL}/@${acct.url}`; acct.note = acct.note || ""; acct.avatar_static = acct.avatar; - acct.header = acct.header || "https://http.cat/404"; - acct.header_static = acct.header || "https://http.cat/404"; + acct.header = acct.header || "/static-assets/transparent.png"; + acct.header_static = acct.header || "/static-assets/transparent.png"; acct.source = { note: acct.note, fields: acct.fields, @@ -164,6 +165,25 @@ export function apiAccountMastodon(router: Router): void { } }, ); + router.get<{ Params: { id: string } }>( + "/v1/accounts/:id/featured_tags", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getAccountFeaturedTags( + convertId(ctx.params.id, IdType.CalckeyId), + ); + ctx.body = data.data.map((tag) => convertFeaturedTag(tag)); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); router.get<{ Params: { id: string } }>( "/v1/accounts/:id/followers", async (ctx) => { @@ -342,6 +362,34 @@ export function apiAccountMastodon(router: Router): void { } }, ); + router.get("/v1/featured_tags", async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getFeaturedTags(); + ctx.body = data.data.map((tag) => convertFeaturedTag(tag)); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get("/v1/followed_tags", async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getFollowedTags(); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); router.get("/v1/bookmarks", async (ctx) => { const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const accessTokens = ctx.headers.authorization; diff --git a/packages/backend/src/server/api/mastodon/endpoints/auth.ts b/packages/backend/src/server/api/mastodon/endpoints/auth.ts index e2cfc47af..b55cb6388 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/auth.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/auth.ts @@ -1,4 +1,4 @@ -import megalodon, { MegalodonInterface } from "@calckey/megalodon"; +import megalodon, { MegalodonInterface } from "megalodon"; import Router from "@koa/router"; import { koaBody } from "koa-body"; import { getClient } from "../ApiMastodonCompatibleService.js"; diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts index c99031b0c..6daad31b6 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts @@ -1,4 +1,4 @@ -import megalodon, { MegalodonInterface } from "@calckey/megalodon"; +import megalodon, { MegalodonInterface } from "megalodon"; import Router from "@koa/router"; import { getClient } from "../ApiMastodonCompatibleService.js"; import { IdType, convertId } from "../../index.js"; diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts index c68a09585..2df4af82a 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts @@ -1,4 +1,4 @@ -import { Entity } from "@calckey/megalodon"; +import { Entity } from "megalodon"; import { fetchMeta } from "@/misc/fetch-meta.js"; import { Users, Notes } from "@/models/index.js"; import { IsNull, MoreThan } from "typeorm"; @@ -24,7 +24,7 @@ export async function getInstance(response: Entity.Instance) { status_count: await totalStatuses, domain_count: response.stats.domain_count, }, - thumbnail: response.thumbnail || "https://http.cat/404", + thumbnail: response.thumbnail || "/static-assets/transparent.png", languages: meta.langs, registrations: !meta.disableRegistration || response.registrations, approval_required: !response.registrations, @@ -96,8 +96,8 @@ export async function getInstance(response: Entity.Instance) { url: `${response.uri}/`, avatar: `${response.uri}/static-assets/badges/info.png`, avatar_static: `${response.uri}/static-assets/badges/info.png`, - header: "https://http.cat/404", - header_static: "https://http.cat/404", + header: "/static-assets/transparent.png", + header_static: "/static-assets/transparent.png", followers_count: -1, following_count: 0, statuses_count: 0, diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts index ac091855f..198717d5c 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts @@ -1,9 +1,9 @@ -import megalodon, { MegalodonInterface } from "@calckey/megalodon"; +import megalodon, { MegalodonInterface } from "megalodon"; import Router from "@koa/router"; import { koaBody } from "koa-body"; import { convertId, IdType } from "../../index.js"; import { getClient } from "../ApiMastodonCompatibleService.js"; -import { convertTimelinesArgsId, toTextWithReaction } from "./timeline.js"; +import { convertTimelinesArgsId } from "./timeline.js"; import { convertNotification } from "../converters.js"; function toLimitToInt(q: any) { if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10); @@ -25,10 +25,6 @@ export function apiNotificationsMastodon(router: Router): void { n = convertNotification(n); if (n.type !== "follow" && n.type !== "follow_request") { if (n.type === "reaction") n.type = "favourite"; - n.status = toTextWithReaction( - n.status ? [n.status] : [], - ctx.hostname, - )[0]; return n; } else { return n; @@ -52,11 +48,13 @@ export function apiNotificationsMastodon(router: Router): void { convertId(ctx.params.id, IdType.CalckeyId), ); const data = convertNotification(dataRaw.data); - if (data.type !== "follow" && data.type !== "follow_request") { - if (data.type === "reaction") data.type = "favourite"; - ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0]; - } else { - ctx.body = data; + ctx.body = data; + if ( + data.type !== "follow" && + data.type !== "follow_request" && + data.type === "reaction" + ) { + data.type = "favourite"; } } catch (e: any) { console.error(e); diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts index e1aec3488..df35b9116 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/search.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts @@ -1,8 +1,8 @@ -import megalodon, { MegalodonInterface } from "@calckey/megalodon"; +import megalodon, { MegalodonInterface } from "megalodon"; import Router from "@koa/router"; import { getClient } from "../ApiMastodonCompatibleService.js"; import axios from "axios"; -import { Converter } from "@calckey/megalodon"; +import { Converter } from "megalodon"; import { convertTimelinesArgsId, limitToInt } from "./timeline.js"; import { convertAccount, convertStatus } from "../converters.js"; @@ -103,7 +103,7 @@ async function getHighlight( i: accessToken, }); const data: MisskeyEntity.Note[] = api.data; - return data.map((note) => Converter.note(note, domain)); + return data.map((note) => new Converter(BASE_URL).note(note, domain)); } catch (e: any) { console.log(e); console.log(e.response.data); @@ -131,7 +131,7 @@ async function getFeaturedUser( return data.map((u) => { return { source: "past_interactions", - account: Converter.userDetail(u, host), + account: new Converter(BASE_URL).userDetail(u, host), }; }); } catch (e: any) { diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index a479140e0..ec978bc84 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -59,6 +59,11 @@ export function apiStatusMastodon(router: Router): void { } if (!body.media_ids) body.media_ids = undefined; if (body.media_ids && !body.media_ids.length) body.media_ids = undefined; + if (body.media_ids) { + body.media_ids = (body.media_ids as string[]).map((p) => + convertId(p, IdType.CalckeyId), + ); + } const { sensitive } = body; body.sensitive = typeof sensitive === "string" ? sensitive === "true" : sensitive; @@ -153,6 +158,24 @@ export function apiStatusMastodon(router: Router): void { } }, ); + router.get<{ Params: { id: string } }>( + "/v1/statuses/:id/history", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getStatusHistory( + convertId(ctx.params.id, IdType.CalckeyId), + ); + ctx.body = data.data.map((account) => convertAccount(account)); + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); router.get<{ Params: { id: string } }>( "/v1/statuses/:id/reblogged_by", async (ctx) => { @@ -431,8 +454,8 @@ export function statusModel( const now = new Date().toISOString(); return { id: "9atm5frjhb", - uri: "https://http.cat/404", // "" - url: "https://http.cat/404", // "", + uri: "/static-assets/transparent.png", // "" + url: "/static-assets/transparent.png", // "", account: { id: "9arzuvv0sw", username: "Reactions", @@ -444,11 +467,11 @@ export function statusModel( following_count: 0, statuses_count: 0, note: "", - url: "https://http.cat/404", + url: "/static-assets/transparent.png", avatar: "/static-assets/badges/info.png", avatar_static: "/static-assets/badges/info.png", - header: "https://http.cat/404", // "" - header_static: "https://http.cat/404", // "" + header: "/static-assets/transparent.png", // "" + header_static: "/static-assets/transparent.png", // "" emojis: [], fields: [], moved: null, diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index b8ef0929e..a155cc93e 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -1,8 +1,5 @@ import Router from "@koa/router"; -import megalodon, { Entity, MegalodonInterface } from "@calckey/megalodon"; import { getClient } from "../ApiMastodonCompatibleService.js"; -import { statusModel } from "./status.js"; -import Autolinker from "autolinker"; import { ParsedUrlQuery } from "querystring"; import { convertAccount, convertList, convertStatus } from "../converters.js"; import { convertId, IdType } from "../../index.js"; @@ -41,66 +38,6 @@ export function convertTimelinesArgsId(q: ParsedUrlQuery) { return q; } -export function toTextWithReaction(status: Entity.Status[], host: string) { - return status.map((t) => { - if (!t) return statusModel(null, null, [], "no content"); - t.quote = null as any; - if (!t.emoji_reactions) return t; - if (t.reblog) t.reblog = toTextWithReaction([t.reblog], host)[0]; - const reactions = t.emoji_reactions.map((r) => { - const emojiNotation = r.url ? `:${r.name.replace("@.", "")}:` : r.name; - return `${emojiNotation} (${r.count}${r.me ? `* ` : ""})`; - }); - const reaction = t.emoji_reactions as Entity.Reaction[]; - const emoji = t.emojis || []; - for (const r of reaction) { - if (!r.url) continue; - emoji.push({ - shortcode: r.name, - url: r.url, - static_url: r.url, - visible_in_picker: true, - category: "", - }); - } - const isMe = reaction.findIndex((r) => r.me) > -1; - const total = reaction.reduce((sum, reaction) => sum + reaction.count, 0); - t.favourited = isMe; - t.favourites_count = total; - t.emojis = emoji; - t.content = `

${autoLinker(t.content, host)}

${reactions.join( - ", ", - )}

`; - return t; - }); -} -export function autoLinker(input: string, host: string) { - return Autolinker.link(input, { - hashtag: "twitter", - mention: "twitter", - email: false, - stripPrefix: false, - replaceFn: function (match) { - switch (match.type) { - case "url": - return true; - case "mention": - console.log("Mention: ", match.getMention()); - console.log("Mention Service Name: ", match.getServiceName()); - return `@${match.getMention()}`; - case "hashtag": - console.log("Hashtag: ", match.getHashtag()); - return `#${match.getHashtag()}`; - } - return false; - }, - }); -} - export function apiTimelineMastodon(router: Router): void { router.get("/v1/timelines/public", async (ctx, reply) => { const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; @@ -115,8 +52,7 @@ export function apiTimelineMastodon(router: Router): void { : await client.getPublicTimeline( convertTimelinesArgsId(argsToBools(limitToInt(query))), ); - let resp = data.data.map((status) => convertStatus(status)); - ctx.body = toTextWithReaction(resp, ctx.hostname); + ctx.body = data.data.map((status) => convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -135,8 +71,7 @@ export function apiTimelineMastodon(router: Router): void { ctx.params.hashtag, convertTimelinesArgsId(argsToBools(limitToInt(ctx.query))), ); - let resp = data.data.map((status) => convertStatus(status)); - ctx.body = toTextWithReaction(resp, ctx.hostname); + ctx.body = data.data.map((status) => convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -153,8 +88,7 @@ export function apiTimelineMastodon(router: Router): void { const data = await client.getHomeTimeline( convertTimelinesArgsId(limitToInt(ctx.query)), ); - let resp = data.data.map((status) => convertStatus(status)); - ctx.body = toTextWithReaction(resp, ctx.hostname); + ctx.body = data.data.map((status) => convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -173,8 +107,7 @@ export function apiTimelineMastodon(router: Router): void { convertId(ctx.params.listId, IdType.CalckeyId), convertTimelinesArgsId(limitToInt(ctx.query)), ); - let resp = data.data.map((status) => convertStatus(status)); - ctx.body = toTextWithReaction(resp, ctx.hostname); + ctx.body = data.data.map((status) => convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index e268b580f..e778c4946 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -25,9 +25,8 @@ 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 "@calckey/megalodon"; +import { Converter } from "megalodon"; import { getClient } from "../mastodon/ApiMastodonCompatibleService.js"; -import { toTextWithReaction } from "../mastodon/endpoints/timeline.js"; /** * Main stream connection @@ -400,12 +399,7 @@ export default class Connection { JSON.stringify({ stream: [payload.id], event: "update", - payload: JSON.stringify( - toTextWithReaction( - [Converter.note(payload.body, this.host)], - this.host, - )[0], - ), + payload: JSON.stringify(Converter.note(payload.body, this.host)), }), ); this.onSubscribeNote({ @@ -415,7 +409,7 @@ export default class Connection { // reaction const client = getClient(this.host, this.accessToken); client.getStatus(payload.id).then((data) => { - const newPost = toTextWithReaction([data.data], this.host); + const newPost = [data.data]; const targetPost = newPost[0]; for (const stream of this.currentSubscribe) { this.wsConnection.send( @@ -442,10 +436,6 @@ export default class Connection { if (payload.id === "user") { const body = Converter.notification(payload.body, this.host); if (body.type === "reaction") body.type = "favourite"; - body.status = toTextWithReaction( - body.status ? [body.status] : [], - "", - )[0]; this.wsConnection.send( JSON.stringify({ stream: ["user"], diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index cd2d13219..efff6dd23 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -22,7 +22,7 @@ import { createTemp } from "@/misc/create-temp.js"; import { publishMainStream } from "@/services/stream.js"; import * as Acct from "@/misc/acct.js"; import { envOption } from "@/env.js"; -import megalodon, { MegalodonInterface } from "@calckey/megalodon"; +import megalodon, { MegalodonInterface } from "megalodon"; import activityPub from "./activitypub.js"; import nodeinfo from "./nodeinfo.js"; import wellKnown from "./well-known.js"; @@ -166,7 +166,7 @@ mastoRouter.post("/oauth/token", async (ctx) => { 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("misskey", BASE_URL, null) as MegalodonInterface; + const client = generator(BASE_URL, null) as MegalodonInterface; let m = null; let token = null; if (body.code) { diff --git a/packages/megalodon/package.json b/packages/megalodon/package.json new file mode 100644 index 000000000..43479b2e7 --- /dev/null +++ b/packages/megalodon/package.json @@ -0,0 +1,81 @@ +{ + "name": "megalodon", + "private": true, + "main": "./lib/src/index.js", + "typings": "./lib/src/index.d.ts", + "scripts": { + "build": "tsc -p ./", + "lint": "eslint --ext .js,.ts src", + "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 000000000..114cb06aa --- /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 000000000..3b905a492 --- /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 000000000..113be823c --- /dev/null +++ b/packages/megalodon/src/converter.ts @@ -0,0 +1,3 @@ +import MisskeyAPI from "./misskey/api_client"; + +export default MisskeyAPI.Converter \ No newline at end of file diff --git a/packages/megalodon/src/default.ts b/packages/megalodon/src/default.ts new file mode 100644 index 000000000..0194b3dcc --- /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 000000000..77f0e71d5 --- /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 000000000..2494916a9 --- /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 000000000..00fa8a04b --- /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 000000000..3af64fcf9 --- /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 000000000..b383f90c5 --- /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 000000000..aab1deade --- /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 000000000..b39cbb8f2 --- /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 000000000..3f2eda58f --- /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 000000000..cdadf1e0f --- /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 000000000..c2bc5a1ef --- /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 000000000..06ae6d7a9 --- /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 000000000..03e4604b0 --- /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 000000000..ffbacb728 --- /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 000000000..070969426 --- /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 000000000..ff857addb --- /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 000000000..e0aaf99b1 --- /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 000000000..2cee0db3c --- /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 000000000..33cb98a10 --- /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 000000000..046912971 --- /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 000000000..d42dfe375 --- /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 + emoji?: string + 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 000000000..69706e8ae --- /dev/null +++ b/packages/megalodon/src/entities/poll.ts @@ -0,0 +1,13 @@ +/// + +namespace Entity { + export type Poll = { + id: string + expires_at: string | null + expired: boolean + multiple: boolean + votes_count: number + options: Array + voted: boolean + } +} diff --git a/packages/megalodon/src/entities/poll_option.ts b/packages/megalodon/src/entities/poll_option.ts new file mode 100644 index 000000000..ae4c63849 --- /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 000000000..cb5797c4c --- /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 000000000..fe7464e8e --- /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 000000000..ccdc2d26a --- /dev/null +++ b/packages/megalodon/src/entities/reaction.ts @@ -0,0 +1,11 @@ +/// + +namespace Entity { + export type Reaction = { + count: number + me: boolean + name: string + 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 000000000..5f10b9c98 --- /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 000000000..28f029981 --- /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 000000000..fe168de67 --- /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 000000000..fb6f63f10 --- /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 000000000..d87cf55d8 --- /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 000000000..76f0bad34 --- /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 000000000..7fd72e20c --- /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 + emoji_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 000000000..30bbee8e6 --- /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 000000000..6de12423c --- /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 000000000..ff5b93381 --- /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 000000000..6fa28e39b --- /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 000000000..4a980d589 --- /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 000000000..39dd4b617 --- /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 000000000..c69be98cd --- /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 000000000..15486ad28 --- /dev/null +++ b/packages/megalodon/src/index.ts @@ -0,0 +1,28 @@ +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 000000000..74966ccf4 --- /dev/null +++ b/packages/megalodon/src/megalodon.ts @@ -0,0 +1,1396 @@ +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> + // ====================================== + // 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, status_id?: string | null): 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>> + // ====================================== + // 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 000000000..5275c70f6 --- /dev/null +++ b/packages/megalodon/src/misskey.ts @@ -0,0 +1,2638 @@ +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"; + +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: string = '' + 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.noteWithMentions(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.noteWithMentions(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 + }) + } + 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.noteWithMentions(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 + }) + } + else { + params = Object.assign(params, { + limit: 40 + }) + } + } + else { + params = Object.assign(params, { + limit: 40 + }) + } + return this.client.post>('/api/users/followers', params).then(res => { + return Object.assign(res, { + data: res.data.map(f => this.converter.follower(f)) + }) + }) + } + + /** + * 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 + }) + } + } + return this.client.post>('/api/users/following', params).then(res => { + return Object.assign(res, { + data: res.data.map(f => this.converter.following(f)) + }) + }) + } + + 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 + }) + } + 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.noteWithMentions(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>> { + 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(res => { + /* + return this.client.post('/api/i/registry/get-all', { + scope: ['client', 'base'], + }).then(ga => { + return Object.assign(res, { + data: this.converter.userPreferences(res.data, ga.data) + }) + }) + */ + + // TODO: + // FIXME: get this from api + return Object.assign(res, { + data: this.converter.userPreferences(res.data, {defaultNoteVisibility: "followers", tutorial: -1}) + }) + }) + } + + // ====================================== + // 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 + } + 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.noteWithMentions(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.noteWithMentions(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())})); + } + + private getFreshAccountCache() :AccountCache { + return { + locks: new AsyncLock(), + accounts: [] + } + } + + public async noteWithMentions(n: MisskeyAPI.Entity.Note, host: string, cache: AccountCache): Promise { + const status = await this.converter.note(n, host); + return status.mentions.length === 0 ? this.addMentionsToStatus(status, cache) : status; + } + + public async addMentionsToStatus(status: Entity.Status, cache: AccountCache) : Promise { + status.mentions = (await this.getMentions(status.plain_content!, cache)).filter(p => p != null); + for (const m of status.mentions.filter((value, index, array) => array.indexOf(value) === index)) { + status.content = status.content.replace(`@${m.acct}`, `@${m.acct}`); + } + return status; + } + + public async getMentions(text: string, cache: AccountCache): Promise { + console.log(`getting mentions for message: '${text}'`); + const mentions :Entity.Mention[] = []; + + if (text == undefined) + return mentions; + + console.log('text is not undefined, continuing'); + + const mentionMatch = text.matchAll(/(?<=^|\s)@(?.*?)(?:@(?.*?)|)(?=\s|$)/g); + + for (const m of mentionMatch) { + if (m.groups == null) + continue; + + const account = await this.getAccountByNameCached(m.groups.user, m.groups.host, cache); + + if (account == null) + continue; + + mentions.push({ + id: account.id, + url: account.url, + username: account.username, + acct: account.acct + }); + } + + 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 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>('/api/notes/conversation', params); + const parents = await Promise.all(conversation.data.map(n => this.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache))); + + const context: Entity.Context = { + ancestors: parents.reverse(), + descendants: this.dfs(await Promise.all(res.data.map(n => this.noteWithMentions(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(res => ({ + ...res, + data: res.data.map(n => this.converter.user(n.user)) + })) + } + + public async getStatusFavouritedBy(_id: string): Promise>> { + return new Promise((_, reject) => { + const err = new NoImplementedError('misskey does not support') + reject(err) + }) + } + + 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] ?? '⭐'); + } + + 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.noteWithMentions(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.noteWithMentions(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.noteWithMentions(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.noteWithMentions(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.noteWithMentions(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.noteWithMentions(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, + filename: file.originalname, + }) + 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 + }) + } + } + 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> { + return new Promise((_, reject) => { + const err = new NoImplementedError('misskey does not support') + reject(err) + }) + } + + /** + * POST /api/notes/polls/vote + */ + public async votePoll(_id: string, choices: Array, status_id?: string | null): Promise> { + if (!status_id) { + return new Promise((_, reject) => { + const err = new ArgumentError('status_id is required') + reject(err) + }) + } + const params = { + noteId: status_id, + choice: choices[0] + } + await this.client.post<{}>('/api/notes/polls/vote', params) + const res = await this.client + .post('/api/notes/show', { + noteId: status_id + }) + .then(async res => { + const note = await this.noteWithMentions(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.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache))) + })) + } + + /** + * 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.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache))) + })) + } + + /** + * 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.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache))) + })) + } + + /** + * 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.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache))) + })) + } + + /** + * 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.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache))) })) + } + + // ====================================== + // 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) + }) + } + + // ====================================== + // 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 + }) + } + 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 + }) + } + return this.client + .post>('/api/i/notifications', params) + .then(res => ({ ...res, data: res.data.map(n => this.converter.notification(n, this.baseUrlToHost(this.baseUrl))) })) + } + + 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> { + 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': { + 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(/^@(?.*?)(?:@(?.*?)|)$/); + 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': { + 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.noteWithMentions(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>> { + 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>> { + 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.noteWithMentions(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.noteWithMentions(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 000000000..1833f9956 --- /dev/null +++ b/packages/megalodon/src/misskey/api_client.ts @@ -0,0 +1,638 @@ +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 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, // FIXME + header_static: this.plcUrl, // FIXME + 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, + locked: u.isLocked, + created_at: u.createdAt, + followers_count: u.followersCount, + following_count: u.followingCount, + statuses_count: u.notesCount, + note: u.description, + 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, g: MisskeyAPI.Entity.GetAll): MegalodonEntity.Preferences => { + return { + "reading:expand:media": "default", + "reading:expand:spoilers": false, + "posting:default:language": u.lang, + "posting:default:sensitive": u.alwaysMarkNsfw, + "posting:default:visibility": this.visibility(g.defaultNoteVisibility) + } + } + + 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): MegalodonEntity.Poll => { + const now = dayjs() + const expire = dayjs(p.expiresAt) + const count = p.choices.reduce((sum, choice) => sum + choice.votes, 0) + return { + 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) + } + } + + 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, + emojis: n.emojis.map(e => this.emoji(e)), + replies_count: n.repliesCount, + reblogs_count: n.renoteCount, + favourites_count: this.getTotalReactions(n.reactions), // FIXME: instead get # of default reaction emoji 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) : null, + application: null, + language: null, + pinned: null, + emoji_reactions: this.mapReactions(n.reactions, n.myReaction), + bookmarked: false, + quote: n.renote && n.text ? this.note(n.renote, host) : null + } + } + + mapReactions = (r: { [key: string]: number }, myReaction?: string): Array => { + return Object.keys(r).map(key => { + if (myReaction && key === myReaction) { + return { + count: r[key], + me: true, + name: key + } + } + return { + count: r[key], + me: false, + name: key + } + }) + } + + 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.EmojiReaction: + return MisskeyNotificationType.Reaction + case NotificationType.Reblog: + return MisskeyNotificationType.Renote + case NotificationType.PollVote: + return MisskeyNotificationType.PollVote + 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.EmojiReaction + case MisskeyNotificationType.PollVote: + return NotificationType.PollVote + 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 (n.reaction) { + notification = Object.assign(notification, { + emoji: n.reaction + }) + } + 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 000000000..c41bd3f1b --- /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 000000000..95100cbf1 --- /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 000000000..40a704b94 --- /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 000000000..9900a777b --- /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 000000000..88ba60040 --- /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 000000000..e545a465d --- /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 000000000..8ed7a54bf --- /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 000000000..f56d21b63 --- /dev/null +++ b/packages/megalodon/src/misskey/entities/field.ts @@ -0,0 +1,6 @@ +namespace MisskeyEntity { + export type Field = { + name: string + value: string + } +} diff --git a/packages/megalodon/src/misskey/entities/file.ts b/packages/megalodon/src/misskey/entities/file.ts new file mode 100644 index 000000000..e9e29ff65 --- /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 000000000..bd2777b2d --- /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 000000000..70ef632e1 --- /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 000000000..927a91354 --- /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 000000000..6a3fe43ad --- /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 000000000..8167d2981 --- /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 000000000..2e99266ab --- /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: 300 + 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 000000000..3cd7ae409 --- /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 000000000..1d7207de1 --- /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 000000000..c331a1ec8 --- /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 000000000..a3f1d971a --- /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 000000000..0c8903529 --- /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 000000000..07653b486 --- /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 000000000..47fe9cf82 --- /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/stats.ts b/packages/megalodon/src/misskey/entities/stats.ts new file mode 100644 index 000000000..7f080efda --- /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 000000000..4ea7bde7c --- /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 000000000..1c6d6431d --- /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 000000000..2cb680863 --- /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 000000000..5b66e95b8 --- /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 000000000..d0bfd15aa --- /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 000000000..9cf3dc58a --- /dev/null +++ b/packages/megalodon/src/misskey/notification.ts @@ -0,0 +1,16 @@ +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 PollVote: MisskeyEntity.NotificationType = 'pollVote' + 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 000000000..d3642864a --- /dev/null +++ b/packages/megalodon/src/misskey/web_socket.ts @@ -0,0 +1,414 @@ +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: number = 60000 + private _pongWaiting: boolean = false + + /** + * @param url Full url of websocket: e.g. wss://misskey.io/streaming + * @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list. + * @param accessToken The access token. + * @param listId This parameter is required when you specify list as channel. + */ + constructor( + url: string, + channel: 'user' | 'localTimeline' | 'hybridTimeline' | 'globalTimeline' | 'conversation' | 'list', + accessToken: string, + listId: string | undefined, + userAgent: string, + proxyConfig: ProxyConfig | false = false, + converter: MisskeyAPI.Converter + ) { + super() + this.url = url + this.parser = new Parser() + this.channel = channel + this.headers = { + 'User-Agent': userAgent + } + if (listId === undefined) { + this.listId = null + } else { + this.listId = listId + } + this.proxyConfig = proxyConfig + this._accessToken = accessToken + this._reconnectInterval = 10000 + this._reconnectMaxAttempts = Infinity + this._reconnectCurrentAttempts = 0 + this._connectionClosed = false + this._channelID = uuid() + this._pongReceivedTimestamp = dayjs() + this._converter = converter + } + + /** + * Start websocket connection. + */ + public start() { + this._connectionClosed = false + this._resetRetryParams() + this._startWebSocketConnection() + } + + private baseUrlToHost(baseUrl: string): string { + return baseUrl.replace('https://', '') + } + + /** + * Reset connection and start new websocket connection. + */ + private _startWebSocketConnection() { + this._resetConnection() + this._setupParser() + this._client = this._connect() + this._bindSocket(this._client) + } + + /** + * Stop current connection. + */ + public stop() { + this._connectionClosed = true + this._resetConnection() + this._resetRetryParams() + } + + /** + * Clean up current connection, and listeners. + */ + private _resetConnection() { + if (this._client) { + this._client.close(1000) + this._client.removeAllListeners() + this._client = null + } + + if (this.parser) { + this.parser.removeAllListeners() + } + } + + /** + * Resets the parameters used in reconnect. + */ + private _resetRetryParams() { + this._reconnectCurrentAttempts = 0 + } + + /** + * Connect to the endpoint. + */ + private _connect(): WS { + let options: WS.ClientOptions = { + headers: this.headers + } + if (this.proxyConfig) { + options = Object.assign(options, { + agent: proxyAgent(this.proxyConfig) + }) + } + const cli: WS = new WS(`${this.url}?i=${this._accessToken}`, options) + return cli + } + + /** + * Connect specified channels in websocket. + */ + private _channel() { + if (!this._client) { + return + } + switch (this.channel) { + case 'conversation': + this._client.send( + JSON.stringify({ + type: 'connect', + body: { + channel: 'main', + id: this._channelID + } + }) + ) + break + case 'user': + this._client.send( + JSON.stringify({ + type: 'connect', + body: { + channel: 'main', + id: this._channelID + } + }) + ) + this._client.send( + JSON.stringify({ + type: 'connect', + body: { + channel: 'homeTimeline', + id: this._channelID + } + }) + ) + break + case 'list': + this._client.send( + JSON.stringify({ + type: 'connect', + body: { + channel: 'userList', + id: this._channelID, + params: { + listId: this.listId + } + } + }) + ) + break + default: + this._client.send( + JSON.stringify({ + type: 'connect', + body: { + channel: this.channel, + id: this._channelID + } + }) + ) + break + } + } + + /** + * Reconnects to the same endpoint. + */ + + private _reconnect() { + setTimeout(() => { + // Skip reconnect when client is connecting. + // https://github.com/websockets/ws/blob/7.2.1/lib/websocket.js#L365 + if (this._client && this._client.readyState === WS.CONNECTING) { + return + } + + if (this._reconnectCurrentAttempts < this._reconnectMaxAttempts) { + this._reconnectCurrentAttempts++ + this._clearBinding() + if (this._client) { + // In reconnect, we want to close the connection immediately, + // because recoonect is necessary when some problems occur. + this._client.terminate() + } + // Call connect methods + console.log('Reconnecting') + this._client = this._connect() + this._bindSocket(this._client) + } + }, this._reconnectInterval) + } + + /** + * Clear binding event for websocket client. + */ + private _clearBinding() { + if (this._client) { + this._client.removeAllListeners('close') + this._client.removeAllListeners('pong') + this._client.removeAllListeners('open') + this._client.removeAllListeners('message') + this._client.removeAllListeners('error') + } + } + + /** + * Bind event for web socket client. + * @param client A WebSocket instance. + */ + private _bindSocket(client: WS) { + client.on('close', (code: number, _reason: Buffer) => { + if (code === 1000) { + this.emit('close', {}) + } else { + console.log(`Closed connection with ${code}`) + if (!this._connectionClosed) { + this._reconnect() + } + } + }) + client.on('pong', () => { + this._pongWaiting = false + this.emit('pong', {}) + this._pongReceivedTimestamp = dayjs() + // It is required to anonymous function since get this scope in checkAlive. + setTimeout(() => this._checkAlive(this._pongReceivedTimestamp), this._heartbeatInterval) + }) + client.on('open', () => { + this.emit('connect', {}) + this._channel() + // Call first ping event. + setTimeout(() => { + client.ping('') + }, 10000) + }) + client.on('message', (data: WS.Data, isBinary: boolean) => { + this.parser.parse(data, isBinary, this._channelID) + }) + client.on('error', (err: Error) => { + this.emit('error', err) + }) + } + + /** + * Set up parser when receive message. + */ + private _setupParser() { + this.parser.on('update', (note: MisskeyAPI.Entity.Note) => { + this.emit('update', this._converter.note(note, this.baseUrlToHost(this.url))) + }) + this.parser.on('notification', (notification: MisskeyAPI.Entity.Notification) => { + this.emit('notification', this._converter.notification(notification, this.baseUrlToHost(this.url))) + }) + this.parser.on('conversation', (note: MisskeyAPI.Entity.Note) => { + this.emit('conversation', this._converter.noteToConversation(note, this.baseUrlToHost(this.url))) + }) + this.parser.on('error', (err: Error) => { + this.emit('parser-error', err) + }) + } + + /** + * Call ping and wait to pong. + */ + private _checkAlive(timestamp: Dayjs) { + const now: Dayjs = dayjs() + // Block multiple calling, if multiple pong event occur. + // It the duration is less than interval, through ping. + if (now.diff(timestamp) > this._heartbeatInterval - 1000 && !this._connectionClosed) { + // Skip ping when client is connecting. + // https://github.com/websockets/ws/blob/7.2.1/lib/websocket.js#L289 + if (this._client && this._client.readyState !== WS.CONNECTING) { + this._pongWaiting = true + this._client.ping('') + setTimeout(() => { + if (this._pongWaiting) { + this._pongWaiting = false + this._reconnect() + } + }, 10000) + } + } + } +} + +/** + * Parser + * This class provides parser for websocket message. + */ +export class Parser extends EventEmitter { + /** + * @param message Message body of websocket. + * @param channelID Parse only messages which has same channelID. + */ + public parse(data: WS.Data, isBinary: boolean, channelID: string) { + const message = isBinary ? data : data.toString() + if (typeof message !== 'string') { + this.emit('heartbeat', {}) + return + } + + if (message === '') { + this.emit('heartbeat', {}) + return + } + + let obj: { + type: string + body: { + id: string + type: string + body: any + } + } + let body: { + id: string + type: string + body: any + } + + try { + obj = JSON.parse(message) + if (obj.type !== 'channel') { + return + } + if (!obj.body) { + return + } + body = obj.body + if (body.id !== channelID) { + return + } + } catch (err) { + this.emit('error', new Error(`Error parsing websocket reply: ${message}, error message: ${err}`)) + return + } + + switch (body.type) { + case 'note': + this.emit('update', body.body as MisskeyAPI.Entity.Note) + break + case 'notification': + this.emit('notification', body.body as MisskeyAPI.Entity.Notification) + break + case 'mention': { + const note = body.body as MisskeyAPI.Entity.Note + if (note.visibility === 'specified') { + this.emit('conversation', note) + } + break + } + // When renote and followed event, the same notification will be received. + case 'renote': + case 'followed': + case 'follow': + case 'unfollow': + case 'receiveFollowRequest': + case 'meUpdated': + case 'readAllNotifications': + case 'readAllUnreadSpecifiedNotes': + case 'readAllAntennas': + case 'readAllUnreadMentions': + case 'unreadNotification': + // Ignore these events + break + default: + this.emit('error', new Error(`Unknown event has received: ${JSON.stringify(body)}`)) + break + } + } +} diff --git a/packages/megalodon/src/notification.ts b/packages/megalodon/src/notification.ts new file mode 100644 index 000000000..9ea3898c6 --- /dev/null +++ b/packages/megalodon/src/notification.ts @@ -0,0 +1,15 @@ +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 EmojiReaction: Entity.NotificationType = 'emoji_reaction' + export const FollowRequest: Entity.NotificationType = 'follow_request' + export const Status: Entity.NotificationType = 'status' + export const PollVote: Entity.NotificationType = 'poll_vote' + export const PollExpired: Entity.NotificationType = 'poll_expired' +} + +export default NotificationType diff --git a/packages/megalodon/src/oauth.ts b/packages/megalodon/src/oauth.ts new file mode 100644 index 000000000..3c3ceb9de --- /dev/null +++ b/packages/megalodon/src/oauth.ts @@ -0,0 +1,109 @@ +/** + * 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 000000000..67abff797 --- /dev/null +++ b/packages/megalodon/src/parser.ts @@ -0,0 +1,86 @@ +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: number = 0 + let offset: number = 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 000000000..7419f664e --- /dev/null +++ b/packages/megalodon/src/proxy_config.ts @@ -0,0 +1,83 @@ +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 000000000..e27596203 --- /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 000000000..896453550 --- /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 000000000..754f6cc28 --- /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.PollVote, + 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.EmojiReaction, + title: 'reaction' + }, + { + event: pollVote, + expected: MegalodonNotificationType.PollVote, + 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 000000000..acaac39ca --- /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.EmojiReaction, + dist: MisskeyNotificationType.Reaction + }, + { + src: MegalodonNotificationType.Reblog, + dist: MisskeyNotificationType.Renote + }, + { + src: MegalodonNotificationType.PollVote, + dist: MisskeyNotificationType.PollVote + }, + { + 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.EmojiReaction + }, + { + src: MisskeyNotificationType.PollVote, + dist: MegalodonNotificationType.PollVote + }, + { + 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 000000000..585210773 --- /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, + emoji_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 000000000..45efcd8a2 --- /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": ["es6", "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 53a12bae8..c5e7f1fad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ importers: '@types/gulp-rename': specifier: 2.0.1 version: 2.0.1 + '@types/node': + specifier: 18.11.18 + version: 18.11.18 chalk: specifier: 4.1.2 version: 4.1.2 @@ -87,9 +90,6 @@ importers: '@bull-board/ui': specifier: 5.2.0 version: 5.2.0 - '@calckey/megalodon': - specifier: 5.2.0 - version: 5.2.0 '@discordapp/twemoji': specifier: 14.1.2 version: 14.1.2 @@ -264,6 +264,9 @@ importers: koa-views: specifier: 7.0.2 version: 7.0.2(@types/koa@2.13.5)(ejs@3.1.9)(pug@3.0.2) + megalodon: + specifier: workspace:* + version: link:../megalodon meilisearch: specifier: 0.33.0 version: 0.33.0 @@ -903,7 +906,7 @@ importers: version: 1.8.0 vite: specifier: 4.3.9 - version: 4.3.9(sass@1.62.1) + version: 4.3.9(@types/node@18.11.18)(sass@1.62.1) vite-plugin-compression: specifier: ^0.5.1 version: 0.5.1(vite@4.3.9) @@ -923,6 +926,124 @@ importers: specifier: 4.1.0 version: 4.1.0(vue@3.3.4) + packages/megalodon: + dependencies: + '@types/oauth': + specifier: ^0.9.0 + version: 0.9.1 + '@types/ws': + specifier: ^8.5.4 + version: 8.5.4 + 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.8 + 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.0 + '@types/form-data': + specifier: ^2.5.0 + version: 2.5.0 + '@types/jest': + specifier: ^29.4.0 + version: 29.4.0 + '@types/node': + specifier: 18.11.18 + version: 18.11.18 + '@types/object-assign-deep': + specifier: ^0.4.0 + version: 0.4.0 + '@types/parse-link-header': + specifier: ^2.0.0 + version: 2.0.0 + '@types/uuid': + specifier: ^9.0.0 + version: 9.0.0 + '@typescript-eslint/eslint-plugin': + specifier: ^5.49.0 + version: 5.49.0(@typescript-eslint/parser@5.49.0)(eslint@8.42.0)(typescript@4.9.4) + '@typescript-eslint/parser': + specifier: ^5.49.0 + version: 5.49.0(eslint@8.42.0)(typescript@4.9.4) + eslint: + specifier: ^8.32.0 + version: 8.42.0 + eslint-config-prettier: + specifier: ^8.6.0 + version: 8.6.0(eslint@8.42.0) + eslint-config-standard: + specifier: ^16.0.3 + version: 16.0.3(eslint-plugin-import@2.27.5)(eslint-plugin-node@11.0.0)(eslint-plugin-promise@6.1.1)(eslint@8.42.0) + eslint-plugin-import: + specifier: ^2.27.5 + version: 2.27.5(@typescript-eslint/parser@5.49.0)(eslint@8.42.0) + eslint-plugin-node: + specifier: ^11.0.0 + version: 11.0.0(eslint@8.42.0) + eslint-plugin-prettier: + specifier: ^4.2.1 + version: 4.2.1(eslint-config-prettier@8.6.0)(eslint@8.42.0)(prettier@2.8.8) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.1.1(eslint@8.42.0) + eslint-plugin-standard: + specifier: ^5.0.0 + version: 5.0.0(eslint@8.42.0) + jest: + specifier: ^29.4.0 + version: 29.4.0(@types/node@18.11.18) + jest-worker: + specifier: ^29.4.0 + version: 29.4.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.0.5(@babel/core@7.22.5)(jest@29.4.0)(typescript@4.9.4) + typedoc: + specifier: ^0.23.24 + version: 0.23.24(typescript@4.9.4) + packages/sw: devDependencies: '@swc/cli': @@ -1173,6 +1294,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.22.5): + resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.22.5): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -1394,30 +1525,6 @@ packages: '@bull-board/api': 5.2.0(@bull-board/ui@5.2.0) dev: false - /@calckey/megalodon@5.2.0: - resolution: {integrity: sha512-9MEjzKJPyd7o5bHGGlNq4oE1tMt22GUJ8o8tZXcXSpXlrSDb2rSwumirM1KXUWTW8G6NGi1leCM59gOBGLko3w==} - engines: {node: '>=15.0.0'} - dependencies: - '@types/oauth': 0.9.1 - '@types/ws': 8.5.4 - axios: 1.2.2 - dayjs: 1.11.8 - form-data: 4.0.0 - https-proxy-agent: 5.0.1 - oauth: 0.10.0 - object-assign-deep: 0.4.0 - parse-link-header: 2.0.0 - socks-proxy-agent: 7.0.0 - typescript: 4.9.4 - uuid: 9.0.0 - ws: 8.12.0 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - dev: false - /@colors/colors@1.5.0: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -1901,6 +2008,18 @@ packages: slash: 3.0.0 dev: true + /@jest/console@29.6.0: + resolution: {integrity: sha512-anb6L1yg7uPQpytNVA5skRaXy3BmrsU8icRhTVNbWdjYWDDfy8M1Kq5HIVRpYoABdbpqsc5Dr+jtu4+qWRQBiQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.0 + '@types/node': 20.3.1 + chalk: 4.1.2 + jest-message-util: 29.6.0 + jest-util: 29.6.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} @@ -1946,6 +2065,48 @@ packages: - utf-8-validate dev: true + /@jest/core@29.6.0: + resolution: {integrity: sha512-5dbMHfY/5R9m8NbgmB3JlxQqooZ/ooPSOiwEQZZ+HODwJTbIu37seVcZNBK29aMdXtjvTRB3f6LCvkKq+r8uQA==} + 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.6.0 + '@jest/reporters': 29.6.0 + '@jest/test-result': 29.6.0 + '@jest/transform': 29.6.0 + '@jest/types': 29.6.0 + '@types/node': 20.3.1 + 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.5.0 + jest-config: 29.6.0(@types/node@20.3.1) + jest-haste-map: 29.6.0 + jest-message-util: 29.6.0 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.0 + jest-resolve-dependencies: 29.6.0 + jest-runner: 29.6.0 + jest-runtime: 29.6.0 + jest-snapshot: 29.6.0 + jest-util: 29.6.0 + jest-validate: 29.6.0 + jest-watcher: 29.6.0 + micromatch: 4.0.5 + pretty-format: 29.6.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - 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} @@ -1956,6 +2117,33 @@ packages: jest-mock: 27.5.1 dev: true + /@jest/environment@29.6.0: + resolution: {integrity: sha512-bUZLYUxYlUIsslBbxII0fq0kr1+friI3Gty+cRLmocGB1jdcAHs7FS8QdCDqedE8q4DZE1g/AJHH6OJZBLGGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.6.0 + '@jest/types': 29.6.0 + '@types/node': 20.3.1 + jest-mock: 29.6.0 + dev: true + + /@jest/expect-utils@29.6.0: + resolution: {integrity: sha512-LLSQQN7oypMSETKoPWpsWYVKJd9LQWmSDDAc4hUQ4JocVC7LAMy9R3ZMhlnLwbcFvQORZnZR7HM893Px6cJhvA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.4.3 + dev: true + + /@jest/expect@29.6.0: + resolution: {integrity: sha512-a7pISPW28Q3c0/pLwz4mQ6tbAI+hc8/0CJp9ix6e9U4dQ6TiHQX82CT5DV5BMWaw8bFH4E6zsfZxXdn6Ka23Bw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.6.0 + jest-snapshot: 29.6.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} @@ -1968,6 +2156,18 @@ packages: jest-util: 27.5.1 dev: true + /@jest/fake-timers@29.6.0: + resolution: {integrity: sha512-nuCU46AsZoskthWSDS2Aj6LARgyNcp5Fjx2qxsO/fPl1Wp1CJ+dBDqs0OkEcJK8FBeV/MbjH5efe79M2sHcV+A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.0 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.3.1 + jest-message-util: 29.6.0 + jest-mock: 29.6.0 + jest-util: 29.6.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} @@ -1977,6 +2177,18 @@ packages: expect: 27.5.1 dev: true + /@jest/globals@29.6.0: + resolution: {integrity: sha512-IQQ3hZ2D/hwEwXSMv5GbfhzdH0nTQR3KPYxnuW6gYWbd6+7/zgMz7Okn6EgBbNtJNONq03k5EKA6HqGyzRbpeg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.0 + '@jest/expect': 29.6.0 + '@jest/types': 29.6.0 + jest-mock: 29.6.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} @@ -2015,6 +2227,43 @@ packages: - supports-color dev: true + /@jest/reporters@29.6.0: + resolution: {integrity: sha512-dWEq4HI0VvHcAD6XTtyBKKARLytyyWPIy1SvGOcU91106MfvHPdxZgupFwVHd8TFpZPpA3SebYjtwS5BUS76Rw==} + 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.6.0 + '@jest/test-result': 29.6.0 + '@jest/transform': 29.6.0 + '@jest/types': 29.6.0 + '@jridgewell/trace-mapping': 0.3.18 + '@types/node': 20.3.1 + chalk: 4.1.2 + collect-v8-coverage: 1.0.1 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-instrument: 5.2.1 + istanbul-lib-report: 3.0.0 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.5 + jest-message-util: 29.6.0 + jest-util: 29.6.0 + jest-worker: 29.6.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.1.0 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/schemas@29.4.3: resolution: {integrity: sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2022,6 +2271,13 @@ packages: '@sinclair/typebox': 0.25.24 dev: true + /@jest/schemas@29.6.0: + resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==} + 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} @@ -2031,6 +2287,15 @@ packages: source-map: 0.6.1 dev: true + /@jest/source-map@29.6.0: + resolution: {integrity: sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.18 + 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} @@ -2041,6 +2306,16 @@ packages: collect-v8-coverage: 1.0.1 dev: true + /@jest/test-result@29.6.0: + resolution: {integrity: sha512-9qLb7xITeyWhM4yatn2muqfomuoCTOhv0QV9i7XiIyYi3QLfnvPv5NeJp5u0PZeutAOROMLKakOkmoAisOr3YQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.6.0 + '@jest/types': 29.6.0 + '@types/istanbul-lib-coverage': 2.0.4 + collect-v8-coverage: 1.0.1 + 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} @@ -2053,6 +2328,16 @@ packages: - supports-color dev: true + /@jest/test-sequencer@29.6.0: + resolution: {integrity: sha512-HYCS3LKRQotKWj2mnA3AN13PPevYZu8MJKm12lzYojpJNnn6kI/3PWmr1At/e3tUu+FHQDiOyaDVuR4EV3ezBw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.6.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.6.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} @@ -2076,6 +2361,29 @@ packages: - supports-color dev: true + /@jest/transform@29.6.0: + resolution: {integrity: sha512-bhP/KxPo3e322FJ0nKAcb6WVK76ZYyQd1lWygJzoSqP8SYMSLdxHqP4wnPTI4WvbB8PKPDV30y5y7Tya4RHOBA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.22.5 + '@jest/types': 29.6.0 + '@jridgewell/trace-mapping': 0.3.18 + 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.6.0 + jest-regex-util: 29.4.3 + jest-util: 29.6.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} @@ -2087,6 +2395,18 @@ packages: chalk: 4.1.2 dev: true + /@jest/types@29.6.0: + resolution: {integrity: sha512-8XCgL9JhqbJTFnMRjEAO+TuW251+MoMd5BSzLiE3vvzpQ8RlBxy8NoyNkDhs3K3OL3HeVinlOl9or5p7GTeOLg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.0 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 20.3.1 + '@types/yargs': 17.0.24 + chalk: 4.1.2 + dev: true + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -2618,6 +2938,10 @@ packages: resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} dev: true + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + /@sindresorhus/is@4.6.0: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -2632,6 +2956,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: @@ -3054,6 +3390,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: @@ -3140,6 +3480,10 @@ packages: '@types/keygrip': 1.0.2 '@types/node': 20.3.1 + /@types/core-js@2.5.0: + resolution: {integrity: sha512-qjkHL3wF0JMHMqgm/kmL8Pf8rIiqvueEiZ0g6NVTcBX1WN46GWDr+V5z+gsHUeL0n8TfAmXnYmF7ajsxmBp4PQ==} + dev: true + /@types/disposable-email-domains@1.0.4: resolution: {integrity: sha512-AmKPD8vBZzvey/jeg+YAIH/xJE3D6edOXz+YUooSCcHesGzFyzke83kj1j4d0LUR9nkSHIRklUVdcAMleuWLpg==} dev: false @@ -3203,6 +3547,13 @@ packages: '@types/node': 20.3.1 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: @@ -3301,6 +3652,13 @@ packages: pretty-format: 27.5.1 dev: true + /@types/jest@29.4.0: + resolution: {integrity: sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==} + dependencies: + expect: 29.6.0 + pretty-format: 29.5.0 + dev: true + /@types/js-yaml@4.0.5: resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==} dev: true @@ -3317,6 +3675,10 @@ packages: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + /@types/jsonld@1.5.8: resolution: {integrity: sha512-4l5t/jDnJpqZ+i7CLTTgPcT5BYXnAnwJupb07aAokPufCV0SjDHcwctUkSTuhIuSU9yHok+WOOngIGCtpL96gw==} dev: true @@ -3541,6 +3903,10 @@ packages: dependencies: '@types/node': 20.3.1 + /@types/object-assign-deep@0.4.0: + resolution: {integrity: sha512-3D0F3rHRNDc8cQSXNzwF1jBrJi28Mdrhc10ZLlqbJWDPYRWTTWB9Tc8JoKrgBvLKioXoPoHT6Uzf3s2F7akCUg==} + dev: true + /@types/offscreencanvas@2019.3.0: resolution: {integrity: sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==} dev: false @@ -3549,6 +3915,10 @@ packages: resolution: {integrity: sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==} dev: false + /@types/parse-link-header@2.0.0: + resolution: {integrity: sha512-KbqcQLdRaawDOfXnwqr6nvhe1MV+Uv/Ww+ViSx7Ujgw9X5qCgObLP52B1ZSJqZD8FK1y/4o+bJQTUrZOynegcg==} + dev: true + /@types/picomatch@2.3.0: resolution: {integrity: sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==} dev: true @@ -3705,6 +4075,10 @@ packages: resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} dev: true + /@types/uuid@9.0.0: + resolution: {integrity: sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==} + dev: true + /@types/vinyl-fs@3.0.2: resolution: {integrity: sha512-ctNcmmzbMIKooXjRkyyUCOu2Z4AyqibL+RhXoF3pb7K7j+ezItnakmpm31LymkYHSIM5ey0tjIFzTvFOTSBCGw==} dependencies: @@ -3755,6 +4129,12 @@ packages: '@types/yargs-parser': 21.0.0 dev: true + /@types/yargs@17.0.24: + resolution: {integrity: sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==} + dependencies: + '@types/yargs-parser': 21.0.0 + dev: true + /@types/yauzl@2.10.0: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true @@ -3763,6 +4143,135 @@ packages: dev: true optional: true + /@typescript-eslint/eslint-plugin@5.49.0(@typescript-eslint/parser@5.49.0)(eslint@8.42.0)(typescript@4.9.4): + resolution: {integrity: sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/parser': 5.49.0(eslint@8.42.0)(typescript@4.9.4) + '@typescript-eslint/scope-manager': 5.49.0 + '@typescript-eslint/type-utils': 5.49.0(eslint@8.42.0)(typescript@4.9.4) + '@typescript-eslint/utils': 5.49.0(eslint@8.42.0)(typescript@4.9.4) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.42.0 + ignore: 5.2.4 + natural-compare-lite: 1.4.0 + regexpp: 3.2.0 + semver: 7.5.1 + tsutils: 3.21.0(typescript@4.9.4) + typescript: 4.9.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.49.0(eslint@8.42.0)(typescript@4.9.4): + resolution: {integrity: sha512-veDlZN9mUhGqU31Qiv2qEp+XrJj5fgZpJ8PW30sHU+j/8/e5ruAhLaVDAeznS7A7i4ucb/s8IozpDtt9NqCkZg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.49.0 + '@typescript-eslint/types': 5.49.0 + '@typescript-eslint/typescript-estree': 5.49.0(typescript@4.9.4) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.42.0 + typescript: 4.9.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@5.49.0: + resolution: {integrity: sha512-clpROBOiMIzpbWNxCe1xDK14uPZh35u4QaZO1GddilEzoCLAEz4szb51rBpdgurs5k2YzPtJeTEN3qVbG+LRUQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.49.0 + '@typescript-eslint/visitor-keys': 5.49.0 + dev: true + + /@typescript-eslint/type-utils@5.49.0(eslint@8.42.0)(typescript@4.9.4): + resolution: {integrity: sha512-eUgLTYq0tR0FGU5g1YHm4rt5H/+V2IPVkP0cBmbhRyEmyGe4XvJ2YJ6sYTmONfjmdMqyMLad7SB8GvblbeESZA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.49.0(typescript@4.9.4) + '@typescript-eslint/utils': 5.49.0(eslint@8.42.0)(typescript@4.9.4) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.42.0 + tsutils: 3.21.0(typescript@4.9.4) + typescript: 4.9.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@5.49.0: + resolution: {integrity: sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/typescript-estree@5.49.0(typescript@4.9.4): + resolution: {integrity: sha512-PBdx+V7deZT/3GjNYPVQv1Nc0U46dAHbIuOG8AZ3on3vuEKiPDwFE/lG1snN2eUB9IhF7EyF7K1hmTcLztNIsA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.49.0 + '@typescript-eslint/visitor-keys': 5.49.0 + debug: 4.3.4(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.1 + tsutils: 3.21.0(typescript@4.9.4) + typescript: 4.9.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@5.49.0(eslint@8.42.0)(typescript@4.9.4): + resolution: {integrity: sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@types/json-schema': 7.0.12 + '@types/semver': 7.5.0 + '@typescript-eslint/scope-manager': 5.49.0 + '@typescript-eslint/types': 5.49.0 + '@typescript-eslint/typescript-estree': 5.49.0(typescript@4.9.4) + eslint: 8.42.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0(eslint@8.42.0) + semver: 7.5.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@5.49.0: + resolution: {integrity: sha512-v9jBMjpNWyn8B6k/Mjt6VbUS4J1GvUlR4x3Y+ibnP1z7y7V4n0WRz+50DY6+Myj0UaXVSuUlHohO+eZ8IJEnkg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.49.0 + eslint-visitor-keys: 3.4.1 + dev: true + /@vitejs/plugin-vue@4.2.3(vite@4.3.9)(vue@3.3.4): resolution: {integrity: sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -3770,7 +4279,7 @@ packages: vite: ^4.0.0 vue: ^3.2.25 dependencies: - vite: 4.3.9(sass@1.62.1) + vite: 4.3.9(@types/node@18.11.18)(sass@1.62.1) vue: 3.3.4 dev: true @@ -4393,6 +4902,13 @@ packages: engines: {node: '>=0.10.0'} dev: true + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: true + /array-each@1.0.1: resolution: {integrity: sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==} engines: {node: '>=0.10.0'} @@ -4403,6 +4919,17 @@ packages: engines: {node: '>=0.10.0'} dev: true + /array-includes@3.1.6: + resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + get-intrinsic: 1.2.1 + is-string: 1.0.7 + dev: true + /array-initial@1.1.0: resolution: {integrity: sha512-BC4Yl89vneCYfpLrs5JU2aAu9/a+xWbeKhvISg9PT7eWFB9UlRvI+rKEtk6mgxWr3dSkk9gQ8hCrdqt06NXPdw==} engines: {node: '>=0.10.0'} @@ -4442,6 +4969,26 @@ packages: engines: {node: '>=0.10.0'} dev: true + /array.prototype.flat@1.3.1: + resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flatmap@1.3.1: + resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + es-shim-unscopables: 1.0.0 + dev: true + /arrgv@1.0.2: resolution: {integrity: sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==} engines: {node: '>=8.0.0'} @@ -4501,6 +5048,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'} @@ -4618,7 +5169,6 @@ packages: /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - dev: false /aws-sdk@2.1277.0: resolution: {integrity: sha512-cEZ0rg0j3STtLX6rba5tHMrV/KrhXKLtSleleF2IdTFzUjqRvxI54Pqc51w2D7tgAPUgEhMB4Q/ruKPqB8w+2Q==} @@ -4715,6 +5265,24 @@ packages: - supports-color dev: true + /babel-jest@29.6.0(@babel/core@7.22.5): + resolution: {integrity: sha512-Jj8Bq2yKsk11XLk06Nm8SdvYkAcecH+GuhxB8DnK5SncjHnJ88TQjSnGgE7jpajpnSvz9DZ6X8hXrDkD/6/TPQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.22.5 + '@jest/transform': 29.6.0 + '@types/babel__core': 7.20.1 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.5.0(@babel/core@7.22.5) + 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'} @@ -4738,6 +5306,16 @@ packages: '@types/babel__traverse': 7.20.1 dev: true + /babel-plugin-jest-hoist@29.5.0: + resolution: {integrity: sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.22.5 + '@babel/types': 7.22.5 + '@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.5): resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} peerDependencies: @@ -4769,6 +5347,17 @@ packages: babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.5) dev: true + /babel-preset-jest@29.5.0(@babel/core@7.22.5): + resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.5 + babel-plugin-jest-hoist: 29.5.0 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.5) + dev: true + /babel-walk@3.0.0-canary-5: resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} engines: {node: '>= 10.0.0'} @@ -5967,6 +6556,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} @@ -6534,6 +7127,13 @@ packages: resolution: {integrity: sha512-LBQvhRw7mznQTPoyZbsmYeNOZt1pN5aCsx4BAU/3siVFuiM9f2oyKzUaB8v1jbxFjE3aYqYiMo63kAL4pHgfWQ==} dev: false + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + /doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -6674,6 +7274,11 @@ packages: /electron-to-chromium@1.4.442: resolution: {integrity: sha512-RkrZF//Ya+0aJq2NM3OdisNh5ZodZq1rdXOS96G8DdDgpDKqKE81yTbbQ3F/4CKm1JBPsGu1Lp/akkna2xO06Q==} + /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'} @@ -6765,10 +7370,74 @@ packages: is-arrayish: 0.2.1 dev: true + /es-abstract@1.21.2: + resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.10 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.0 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.7 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.6 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.9 + dev: true + /es-module-lexer@1.3.0: resolution: {integrity: sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==} dev: true + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: true + + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + /es5-ext@0.10.62: resolution: {integrity: sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==} engines: {node: '>=0.10'} @@ -6883,6 +7552,29 @@ packages: optionalDependencies: source-map: 0.6.1 + /eslint-config-prettier@8.6.0(eslint@8.42.0): + resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.42.0 + dev: true + + /eslint-config-standard@16.0.3(eslint-plugin-import@2.27.5)(eslint-plugin-node@11.0.0)(eslint-plugin-promise@6.1.1)(eslint@8.42.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.42.0 + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.49.0)(eslint@8.42.0) + eslint-plugin-node: 11.0.0(eslint@8.42.0) + eslint-plugin-promise: 6.1.1(eslint@8.42.0) + dev: true + /eslint-formatter-pretty@4.1.0: resolution: {integrity: sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==} engines: {node: '>=10'} @@ -6897,6 +7589,139 @@ packages: supports-hyperlinks: 2.3.0 dev: true + /eslint-import-resolver-node@0.3.7: + resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} + dependencies: + debug: 3.2.7(supports-color@8.1.1) + is-core-module: 2.12.1 + resolve: 1.22.2 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.49.0)(eslint-import-resolver-node@0.3.7)(eslint@8.42.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.49.0(eslint@8.42.0)(typescript@4.9.4) + debug: 3.2.7(supports-color@8.1.1) + eslint: 8.42.0 + eslint-import-resolver-node: 0.3.7 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-es@3.0.1(eslint@8.42.0): + resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + eslint: 8.42.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + dev: true + + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.49.0)(eslint@8.42.0): + resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 5.49.0(eslint@8.42.0)(typescript@4.9.4) + array-includes: 3.1.6 + array.prototype.flat: 1.3.1 + array.prototype.flatmap: 1.3.1 + debug: 3.2.7(supports-color@8.1.1) + doctrine: 2.1.0 + eslint: 8.42.0 + eslint-import-resolver-node: 0.3.7 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.49.0)(eslint-import-resolver-node@0.3.7)(eslint@8.42.0) + has: 1.0.3 + is-core-module: 2.12.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.values: 1.1.6 + resolve: 1.22.2 + semver: 6.3.0 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-node@11.0.0(eslint@8.42.0): + resolution: {integrity: sha512-chUs/NVID+sknFiJzxoN9lM7uKSOEta8GC8365hw1nDfwIPIjjpRSwwPvQanWv8dt/pDe9EV4anmVSwdiSndNg==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=5.16.0' + dependencies: + eslint: 8.42.0 + eslint-plugin-es: 3.0.1(eslint@8.42.0) + eslint-utils: 2.1.0 + ignore: 5.2.4 + minimatch: 3.1.2 + resolve: 1.22.2 + semver: 6.3.0 + dev: true + + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.6.0)(eslint@8.42.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.42.0 + eslint-config-prettier: 8.6.0(eslint@8.42.0) + prettier: 2.8.8 + prettier-linter-helpers: 1.0.0 + dev: true + + /eslint-plugin-promise@6.1.1(eslint@8.42.0): + resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.42.0 + dev: true + + /eslint-plugin-standard@5.0.0(eslint@8.42.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.42.0 + dev: true + /eslint-rule-docs@1.1.235: resolution: {integrity: sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==} dev: true @@ -6916,10 +7741,31 @@ packages: esrecurse: 4.3.0 estraverse: 5.3.0 + /eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + dependencies: + eslint-visitor-keys: 1.3.0 + dev: true + + /eslint-utils@3.0.0(eslint@8.42.0): + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.42.0 + eslint-visitor-keys: 2.1.0 + dev: true + /eslint-visitor-keys@1.3.0: resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} engines: {node: '>=4'} - dev: false + + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true /eslint-visitor-keys@3.4.1: resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} @@ -7171,6 +8017,18 @@ packages: jest-message-util: 27.5.1 dev: true + /expect@29.6.0: + resolution: {integrity: sha512-AV+HaBtnDJ2YEUhPPo25HyUHBLaetM+y/Dq6pEC8VPQyt1dK+k8MfGkMy46djy2bddcqESc1kl4/K1uLWSfk9g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.6.0 + '@types/node': 20.3.1 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.6.0 + jest-message-util: 29.6.0 + jest-util: 29.6.0 + dev: true + /exponential-backoff@3.1.1: resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} dev: false @@ -7549,7 +8407,6 @@ packages: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 - dev: false /for-in@1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} @@ -7602,7 +8459,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==} @@ -7731,6 +8587,20 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /function.prototype.name@1.1.5: + resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + /gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} @@ -7832,6 +8702,14 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: true + /get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} @@ -7993,6 +8871,13 @@ packages: dependencies: type-fest: 0.20.2 + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.0 + dev: true + /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -8032,7 +8917,6 @@ packages: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.1 - dev: false /got@11.8.5: resolution: {integrity: sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==} @@ -8191,6 +9075,10 @@ packages: ansi-regex: 2.1.1 dev: true + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + /has-flag@1.0.0: resolution: {integrity: sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==} engines: {node: '>=0.10.0'} @@ -8614,6 +9502,15 @@ packages: executioner: 2.0.1 dev: true + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + /interpret@1.4.0: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} @@ -8719,6 +9616,14 @@ packages: has-tostringtag: 1.0.0 dev: false + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.10 + dev: true + /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: true @@ -8727,19 +9632,32 @@ packages: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} dev: false + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} dependencies: binary-extensions: 2.2.0 + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + /is-buffer@1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - dev: false /is-ci@3.0.1: resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} @@ -8767,6 +9685,13 @@ packages: kind-of: 6.0.3 dev: true + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-descriptor@0.1.6: resolution: {integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==} engines: {node: '>=0.10.0'} @@ -8879,6 +9804,18 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-number@3.0.0: resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} engines: {node: '>=0.10.0'} @@ -8954,6 +9891,12 @@ packages: is-unc-path: 1.0.0 dev: true + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + /is-stream@1.1.0: resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} engines: {node: '>=0.10.0'} @@ -8969,6 +9912,13 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-svg@2.1.0: resolution: {integrity: sha512-Ya1giYJUkcL/94quj0+XGcmts6cETPBW1MiFz1ReJrnDJ680F52qpAEGAEGU0nq96FRGIGPx6Yo1CyPXcOoyGw==} engines: {node: '>=0.10.0'} @@ -8983,6 +9933,13 @@ packages: fast-xml-parser: 3.21.1 dev: false + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + /is-typed-array@1.1.10: resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} engines: {node: '>= 0.4'} @@ -8992,7 +9949,6 @@ packages: for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 - dev: false /is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} @@ -9027,6 +9983,12 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + /is-whitespace@0.3.0: resolution: {integrity: sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==} engines: {node: '>=0.10.0'} @@ -9143,6 +10105,14 @@ packages: throat: 6.0.2 dev: true + /jest-changed-files@29.5.0: + resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + 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} @@ -9170,6 +10140,34 @@ packages: - supports-color dev: true + /jest-circus@29.6.0: + resolution: {integrity: sha512-LtG45qEKhse2Ws5zNR4DnZATReLGQXzBZGZnJ0DU37p6d4wDhu41vvczCQ3Ou+llR6CRYDBshsubV7H4jZvIkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.0 + '@jest/expect': 29.6.0 + '@jest/test-result': 29.6.0 + '@jest/types': 29.6.0 + '@types/node': 20.3.1 + chalk: 4.1.2 + co: 4.6.0 + dedent: 0.7.0 + is-generator-fn: 2.1.0 + jest-each: 29.6.0 + jest-matcher-utils: 29.6.0 + jest-message-util: 29.6.0 + jest-runtime: 29.6.0 + jest-snapshot: 29.6.0 + jest-util: 29.6.0 + p-limit: 3.1.0 + pretty-format: 29.6.0 + pure-rand: 6.0.2 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - 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} @@ -9200,6 +10198,34 @@ packages: - utf-8-validate dev: true + /jest-cli@29.6.0(@types/node@18.11.18): + resolution: {integrity: sha512-WvZIaanK/abkw6s01924DQ2QLwM5Q4Y4iPbSDb9Zg6smyXGqqcPQ7ft9X8D7B0jICz312eSzM6UlQNxuZJBrMw==} + 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.6.0 + '@jest/test-result': 29.6.0 + '@jest/types': 29.6.0 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + import-local: 3.1.0 + jest-config: 29.6.0(@types/node@18.11.18) + jest-util: 29.6.0 + jest-validate: 29.6.0 + prompts: 2.4.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - 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} @@ -9241,6 +10267,84 @@ packages: - utf-8-validate dev: true + /jest-config@29.6.0(@types/node@18.11.18): + resolution: {integrity: sha512-fKA4jM91PDqWVkMpb1FVKxIuhg3hC6hgaen57cr1rRZkR96dCatvJZsk3ik7/GNu9ERj9wgAspOmyvkFoGsZhA==} + 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.5 + '@jest/test-sequencer': 29.6.0 + '@jest/types': 29.6.0 + '@types/node': 18.11.18 + babel-jest: 29.6.0(@babel/core@7.22.5) + 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.6.0 + jest-environment-node: 29.6.0 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.0 + jest-runner: 29.6.0 + jest-util: 29.6.0 + jest-validate: 29.6.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.6.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-config@29.6.0(@types/node@20.3.1): + resolution: {integrity: sha512-fKA4jM91PDqWVkMpb1FVKxIuhg3hC6hgaen57cr1rRZkR96dCatvJZsk3ik7/GNu9ERj9wgAspOmyvkFoGsZhA==} + 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.5 + '@jest/test-sequencer': 29.6.0 + '@jest/types': 29.6.0 + '@types/node': 20.3.1 + babel-jest: 29.6.0(@babel/core@7.22.5) + 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.6.0 + jest-environment-node: 29.6.0 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.0 + jest-runner: 29.6.0 + jest-util: 29.6.0 + jest-validate: 29.6.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.6.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - 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} @@ -9261,6 +10365,16 @@ packages: pretty-format: 29.5.0 dev: true + /jest-diff@29.6.0: + resolution: {integrity: sha512-ZRm7cd2m9YyZ0N3iMyuo1iUiprxQ/MFpYWXzEEj7hjzL3WnDffKW8192XBDcrAI8j7hnrM1wed3bL/oEnYF/8w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.4.3 + jest-get-type: 29.4.3 + pretty-format: 29.6.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} @@ -9268,6 +10382,13 @@ packages: detect-newline: 3.1.0 dev: true + /jest-docblock@29.4.3: + resolution: {integrity: sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==} + 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} @@ -9279,6 +10400,17 @@ packages: pretty-format: 27.5.1 dev: true + /jest-each@29.6.0: + resolution: {integrity: sha512-d0Jem4RBAlFUyV6JSXPSHVUpNo5RleSj+iJEy1G3+ZCrzHDjWs/1jUfrbnJKHdJdAx5BCEce/Ju379WqHhQk4w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.0 + chalk: 4.1.2 + jest-get-type: 29.4.3 + jest-util: 29.6.0 + pretty-format: 29.6.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} @@ -9309,6 +10441,18 @@ packages: jest-util: 27.5.1 dev: true + /jest-environment-node@29.6.0: + resolution: {integrity: sha512-BOf5Q2/nFCdBOnyBM5c5/6DbdQYgc+0gyUQ8l8qhUAB8O7pM+4QJXIXJsRZJaxd5SHV6y5VArTVhOfogoqcP8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.0 + '@jest/fake-timers': 29.6.0 + '@jest/types': 29.6.0 + '@types/node': 20.3.1 + jest-mock: 29.6.0 + jest-util: 29.6.0 + dev: true + /jest-fetch-mock@3.0.3: resolution: {integrity: sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==} dependencies: @@ -9348,6 +10492,25 @@ packages: fsevents: 2.3.2 dev: true + /jest-haste-map@29.6.0: + resolution: {integrity: sha512-dY1DKufptj7hcJSuhpqlYPGcnN3XjlOy/g0jinpRTMsbb40ivZHiuIPzeminOZkrek8C+oDxC54ILGO3vMLojg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.0 + '@types/graceful-fs': 4.1.6 + '@types/node': 20.3.1 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.4.3 + jest-util: 29.6.0 + jest-worker: 29.6.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} @@ -9381,6 +10544,14 @@ packages: pretty-format: 27.5.1 dev: true + /jest-leak-detector@29.6.0: + resolution: {integrity: sha512-JdV6EZOPxHR1gd6ccxjNowuROkT2jtGU5G/g58RcJX1xe5mrtLj0g6/ZkyMoXF4cs+tTkHMFX6pcIrB1QPQwCw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.4.3 + pretty-format: 29.6.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} @@ -9391,6 +10562,16 @@ packages: pretty-format: 27.5.1 dev: true + /jest-matcher-utils@29.6.0: + resolution: {integrity: sha512-oSlqfGN+sbkB2Q5um/zL7z80w84FEAcLKzXBZIPyRk2F2Srg1ubhrHVKW68JCvb2+xKzAeGw35b+6gciS24PHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.6.0 + jest-get-type: 29.4.3 + pretty-format: 29.6.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} @@ -9406,6 +10587,21 @@ packages: stack-utils: 2.0.6 dev: true + /jest-message-util@29.6.0: + resolution: {integrity: sha512-mkCp56cETbpoNtsaeWVy6SKzk228mMi9FPHSObaRIhbR2Ujw9PqjW/yqVHD2tN1bHbC8ol6h3UEo7dOPmIYwIA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.22.5 + '@jest/types': 29.6.0 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.6.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} @@ -9414,6 +10610,15 @@ packages: '@types/node': 20.3.1 dev: true + /jest-mock@29.6.0: + resolution: {integrity: sha512-2Pb7R2w24Q0aUVn+2/vdRDL6CqGqpheDZy7zrXav8FotOpSGw/4bS2hyVoKHMEx4xzOn6EyCAGwc5czWxXeN7w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.0 + '@types/node': 20.3.1 + jest-util: 29.6.0 + dev: true + /jest-pnp-resolver@1.2.3(jest-resolve@27.5.1): resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} @@ -9426,11 +10631,28 @@ packages: jest-resolve: 27.5.1 dev: true + /jest-pnp-resolver@1.2.3(jest-resolve@29.6.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.6.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.4.3: + resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} + 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} @@ -9442,6 +10664,16 @@ packages: - supports-color dev: true + /jest-resolve-dependencies@29.6.0: + resolution: {integrity: sha512-eOfPog9K3hJdJk/3i6O6bQhXS+3uXhMDkLJGX+xmMPp7T1d/zdcFofbDnHgNoEkhD/mSimC5IagLEP7lpLLu/A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.4.3 + jest-snapshot: 29.6.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} @@ -9458,6 +10690,21 @@ packages: slash: 3.0.0 dev: true + /jest-resolve@29.6.0: + resolution: {integrity: sha512-+hrpY4LzAONoZA/rvB6rnZLkOSA6UgJLpdCWrOZNSgGxWMumzRLu7dLUSCabAHzoHIDQ9qXfr3th1zYNJ0E8sQ==} + 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.6.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.6.0) + jest-util: 29.6.0 + jest-validate: 29.6.0 + resolve: 1.22.2 + 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} @@ -9490,6 +10737,35 @@ packages: - utf-8-validate dev: true + /jest-runner@29.6.0: + resolution: {integrity: sha512-4fZuGV2lOxS2BiqEG9/AI8E6O+jo+QZjMVcgi1x5E6aDql0Gd/EFIbUQ0pSS09y8cya1vJB/qC2xsE468jqtSg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.6.0 + '@jest/environment': 29.6.0 + '@jest/test-result': 29.6.0 + '@jest/transform': 29.6.0 + '@jest/types': 29.6.0 + '@types/node': 20.3.1 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.4.3 + jest-environment-node: 29.6.0 + jest-haste-map: 29.6.0 + jest-leak-detector: 29.6.0 + jest-message-util: 29.6.0 + jest-resolve: 29.6.0 + jest-runtime: 29.6.0 + jest-util: 29.6.0 + jest-watcher: 29.6.0 + jest-worker: 29.6.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} @@ -9520,6 +10796,36 @@ packages: - supports-color dev: true + /jest-runtime@29.6.0: + resolution: {integrity: sha512-5FavYo3EeXLHIvnJf+r7Cj0buePAbe4mzRB9oeVxDS0uVmouSBjWeGgyRjZkw7ArxOoZI8gO6f8SGMJ2HFlwwg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.0 + '@jest/fake-timers': 29.6.0 + '@jest/globals': 29.6.0 + '@jest/source-map': 29.6.0 + '@jest/test-result': 29.6.0 + '@jest/transform': 29.6.0 + '@jest/types': 29.6.0 + '@types/node': 20.3.1 + chalk: 4.1.2 + cjs-module-lexer: 1.2.3 + collect-v8-coverage: 1.0.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.6.0 + jest-message-util: 29.6.0 + jest-mock: 29.6.0 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.0 + jest-snapshot: 29.6.0 + jest-util: 29.6.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} @@ -9558,6 +10864,35 @@ packages: - supports-color dev: true + /jest-snapshot@29.6.0: + resolution: {integrity: sha512-H3kUE9NwWDEDoutcOSS921IqdlkdjgnMdj1oMyxAHNflscdLc9dB8OudZHV6kj4OHJxbMxL8CdI5DlwYrs4wQg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.22.5 + '@babel/generator': 7.22.5 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.5) + '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.22.5) + '@babel/types': 7.22.5 + '@jest/expect-utils': 29.6.0 + '@jest/transform': 29.6.0 + '@jest/types': 29.6.0 + '@types/prettier': 2.7.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.5) + chalk: 4.1.2 + expect: 29.6.0 + graceful-fs: 4.2.11 + jest-diff: 29.6.0 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.6.0 + jest-message-util: 29.6.0 + jest-util: 29.6.0 + natural-compare: 1.4.0 + pretty-format: 29.6.0 + semver: 7.5.3 + 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} @@ -9570,6 +10905,18 @@ packages: picomatch: 2.3.1 dev: true + /jest-util@29.6.0: + resolution: {integrity: sha512-S0USx9YwcvEm4pQ5suisVm/RVxBmi0GFR7ocJhIeaCuW5AXnAnffXbaVKvIFodyZNOc9ygzVtTxmBf40HsHXaA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.0 + '@types/node': 20.3.1 + 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} @@ -9582,6 +10929,18 @@ packages: pretty-format: 27.5.1 dev: true + /jest-validate@29.6.0: + resolution: {integrity: sha512-MLTrAJsb1+W7svbeZ+A7pAnyXMaQrjvPDKCy7OlfsfB6TMVc69v7WjUWfiR6r3snULFWZASiKgvNVDuATta1dg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.0 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.4.3 + leven: 3.1.0 + pretty-format: 29.6.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} @@ -9595,6 +10954,20 @@ packages: string-length: 4.0.2 dev: true + /jest-watcher@29.6.0: + resolution: {integrity: sha512-LdsQqFNX60mRdRRe+zsELnYRH1yX6KL+ukbh+u6WSQeTheZZe1TlLJNKRQiZ7e0VbvMkywmMWL/KV35noOJCcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.6.0 + '@jest/types': 29.6.0 + '@types/node': 20.3.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.6.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: @@ -9613,6 +10986,26 @@ packages: supports-color: 8.1.1 dev: true + /jest-worker@29.4.0: + resolution: {integrity: sha512-dICMQ+Q4W0QVMsaQzWlA1FVQhKNz7QcDCOGtbk1GCAd0Lai+wdkQvfmQwL4MjGumineh1xz+6M5oMj3rfWS02A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 20.3.1 + jest-util: 29.6.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest-worker@29.6.0: + resolution: {integrity: sha512-oiQHH1SnKmZIwwPnpOrXTq4kHBk3lKGY/07DpnH0sAu+x7J8rXlbLDROZsU6vy9GwB0hPiZeZpu6YlJ48QoKcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 20.3.1 + jest-util: 29.6.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} @@ -9634,6 +11027,26 @@ packages: - utf-8-validate dev: true + /jest@29.4.0(@types/node@18.11.18): + resolution: {integrity: sha512-Zfd4UzNxPkSoHRBkg225rBjQNa6pVqbh20MGniAzwaOzYLd+pQUcAwH+WPxSXxKFs+QWYfPYIq9hIVSmdVQmPA==} + 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.6.0 + '@jest/types': 29.6.0 + import-local: 3.1.0 + jest-cli: 29.6.0(@types/node@18.11.18) + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + dev: true + /jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} dev: true @@ -9856,11 +11269,22 @@ packages: webpack: 5.85.1(@swc/core@1.3.62)(webpack-cli@5.1.3) dev: true + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + /json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: 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: @@ -10516,6 +11940,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'} @@ -10610,6 +12038,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'} @@ -11055,6 +12489,10 @@ packages: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} dev: false + /natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + dev: true + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -11439,6 +12877,15 @@ packages: make-iterator: 1.0.1 dev: true + /object.values@1.1.6: + resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + /oblivious-set@1.1.1: resolution: {integrity: sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w==} dev: true @@ -12284,6 +13731,13 @@ packages: engines: {node: '>=0.10.0'} dev: true + /prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + dependencies: + fast-diff: 1.3.0 + dev: true + /prettier-plugin-vue@1.1.6: resolution: {integrity: sha512-trQ/oY+6hSsGe2zPIFThXMIM0TbxMEbk2VOrKjwHWuSz7OEo0rnumbz9a47OxVPCaAnGY9vZG8qDTiTmk9bq0A==} dependencies: @@ -12325,6 +13779,15 @@ packages: react-is: 18.2.0 dev: true + /pretty-format@29.6.0: + resolution: {integrity: sha512-XH+D4n7Ey0iSR6PdAnBs99cWMZdGsdKrR33iUHQNr79w1szKTCIZDVdXuccAsHVwDBp0XeWPfNEoaxP9EZgRmQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.0 + 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'} @@ -12554,6 +14017,10 @@ packages: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} + /pure-rand@6.0.2: + resolution: {integrity: sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==} + dev: true + /pureimage@0.3.15: resolution: {integrity: sha512-QpQYEV8nxVb84en7D0nKXwG0bdmwmlsSg9QnqxpEOExvUXdbmo6Lw/UoxSXD9z+ryvWDkgWqZsIM3iPCAh4dXg==} engines: {node: '>=0.8'} @@ -12893,6 +14360,20 @@ packages: safe-regex: 1.1.0 dev: true + /regexp.prototype.flags@1.5.0: + resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + functions-have-names: 1.2.3 + dev: true + + /regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + /remove-bom-buffer@3.0.0: resolution: {integrity: sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==} engines: {node: '>=0.10.0'} @@ -13062,6 +14543,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: @@ -13185,6 +14671,14 @@ packages: /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: true + /safe-regex@1.1.0: resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} dependencies: @@ -13406,6 +14900,14 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + /shiki@0.12.1: + resolution: {integrity: sha512-aieaV1m349rZINEBkjxh2QbBvFFQOlgqYTNtCal82hHj4dDZ76oMlQIX+C7ryerBTDiga3e5NfH6smjdJ02BbQ==} + dependencies: + jsonc-parser: 3.2.0 + vscode-oniguruma: 1.7.0 + vscode-textmate: 8.0.0 + dev: true + /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -13575,6 +15077,13 @@ packages: urix: 0.1.0 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: @@ -13801,6 +15310,31 @@ packages: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + /string.prototype.trim@1.2.7: + resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /string.prototype.trimend@1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /string.prototype.trimstart@1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + /string_decoder@0.10.31: resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} dev: false @@ -14411,6 +15945,40 @@ packages: yargs-parser: 20.2.9 dev: true + /ts-jest@29.0.5(@babel/core@7.22.5)(jest@29.4.0)(typescript@4.9.4): + resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.22.5 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.4.0(@types/node@18.11.18) + jest-util: 29.6.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.1 + typescript: 4.9.4 + yargs-parser: 21.1.1 + dev: true + /ts-loader@9.4.3(typescript@5.1.3)(webpack@5.85.1): resolution: {integrity: sha512-n3hBnm6ozJYzwiwt5YRiJZkzktftRpMiBApHaJPoWLA+qetQBAXkHqCLM6nwSdRDimqVtA5ocIkcTRLMTt7yzA==} engines: {node: '>=12.0.0'} @@ -14500,6 +16068,15 @@ packages: plimit-lit: 1.5.0 dev: true + /tsconfig-paths@3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + /tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -14523,6 +16100,10 @@ packages: read-pkg-up: 7.0.1 dev: true + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + /tslib@2.6.0: resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} @@ -14531,6 +16112,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 + /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: @@ -14601,6 +16192,14 @@ packages: /type@2.7.2: resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.10 + dev: true + /typedarray-to-buffer@3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} dependencies: @@ -14609,6 +16208,20 @@ packages: /typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + /typedoc@0.23.24(typescript@4.9.4): + resolution: {integrity: sha512-bfmy8lNQh+WrPYcJbtjQ6JEEsVl/ce1ZIXyXhyW+a1vFrjO39t6J8sL/d6FfAGrJTc7McCXgk9AanYBSNvLdIA==} + engines: {node: '>= 14.14'} + hasBin: true + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x + dependencies: + lunr: 2.3.9 + marked: 4.3.0 + minimatch: 5.1.6 + shiki: 0.12.1 + typescript: 4.9.4 + dev: true + /typeorm@0.3.11(ioredis@5.3.2)(pg@8.11.0)(ts-node@10.9.1): resolution: {integrity: sha512-pzdOyWbVuz/z8Ww6gqvBW4nylsM0KLdUCDExr2gR20/x1khGSVxQkjNV/3YqliG90jrWzrknYbYscpk8yxFJVg==} engines: {node: '>= 12.9.0'} @@ -14712,6 +16325,15 @@ packages: hasBin: true dev: false + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + /unc-path-regex@0.1.2: resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} engines: {node: '>=0.10.0'} @@ -14924,6 +16546,15 @@ packages: source-map: 0.7.4 dev: true + /v8-to-istanbul@9.1.0: + resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.18 + '@types/istanbul-lib-coverage': 2.0.4 + convert-source-map: 1.9.0 + dev: true + /v8flags@3.2.0: resolution: {integrity: sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==} engines: {node: '>= 0.10'} @@ -15031,12 +16662,12 @@ packages: chalk: 4.1.2 debug: 4.3.4(supports-color@8.1.1) fs-extra: 10.1.0 - vite: 4.3.9(sass@1.62.1) + vite: 4.3.9(@types/node@18.11.18)(sass@1.62.1) transitivePeerDependencies: - supports-color dev: true - /vite@4.3.9(sass@1.62.1): + /vite@4.3.9(@types/node@18.11.18)(sass@1.62.1): resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -15061,6 +16692,7 @@ packages: terser: optional: true dependencies: + '@types/node': 18.11.18 esbuild: 0.17.19 postcss: 8.4.24 rollup: 3.23.1 @@ -15073,6 +16705,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-isyourpasswordsafe@2.0.0: resolution: {integrity: sha512-j3ORj18R9AgFiP2UOM35KuZbSeJAUiwCSyeRBFN3CGFYTJSKsxqU9qGqOHOz6OhLAYKMTin8JOmqugAbF9O+Bg==} dependencies: @@ -15367,6 +17007,16 @@ packages: engines: {node: '>=0.6.0'} dev: true + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + /which-module@1.0.0: resolution: {integrity: sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==} dev: true @@ -15385,7 +17035,6 @@ packages: gopd: 1.0.1 has-tostringtag: 1.0.0 is-typed-array: 1.1.10 - dev: false /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} @@ -15472,6 +17121,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} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2bf4474f0..b449f9f23 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,3 +4,4 @@ packages: - 'packages/client' - 'packages/sw' - 'packages/calckey-js' + - 'packages/megalodon'