From 268b7aeb3f03c57c6a34e0461d1eb4d54cf56392 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 17:16:25 +0800 Subject: [PATCH 001/110] refactor: Fix type errors of mfm.ts --- packages/client/src/components/mfm.ts | 229 +++++++++++++++----------- 1 file changed, 137 insertions(+), 92 deletions(-) diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts index 0800e8270b..f2b100a207 100644 --- a/packages/client/src/components/mfm.ts +++ b/packages/client/src/components/mfm.ts @@ -1,6 +1,6 @@ import { defineComponent, h } from "vue"; import * as mfm from "mfm-js"; -import type { VNode, PropType } from "vue"; +import type { PropType, VNodeArrayChildren } from "vue"; import MkUrl from "@/components/global/MkUrl.vue"; import MkLink from "@/components/MkLink.vue"; import MkMention from "@/components/MkMention.vue"; @@ -30,11 +30,12 @@ export default defineComponent({ default: false, }, author: { - type: Object, + type: Object as PropType<entities.User>, default: null, }, + // TODO: This variable is not used in the code and may be removed i: { - type: Object, + type: Object as PropType<entities.User>, default: null, }, customEmojis: { @@ -58,14 +59,16 @@ export default defineComponent({ const ast = (isPlain ? mfm.parseSimple : mfm.parse)(this.text); - const validTime = (t: string | null | undefined) => { + const validTime = (t: string | null | undefined | boolean) => { if (t == null) return null; + if (typeof t !== "string") return null; return t.match(/^[0-9.]+s$/) ? t : null; }; - const validNumber = (n: string | null | undefined) => { + const validNumber = (n: string | null | undefined | boolean) => { if (n == null) return null; - const parsed = parseFloat(n); + if (typeof n !== "string") return null; + const parsed = Number.parseFloat(n); return !Number.isNaN(parsed) && Number.isFinite(parsed) && parsed > 0; }; // const validEase = (e: string | null | undefined) => { @@ -77,13 +80,13 @@ export default defineComponent({ const genEl = (ast: mfm.MfmNode[]) => concat( - ast.map((token, index): VNode[] => { + ast.map((token, index): VNodeArrayChildren => { switch (token.type) { case "text": { const text = token.props.text.replace(/(\r\n|\n|\r)/g, "\n"); if (!this.plain) { - const res = []; + const res: VNodeArrayChildren = []; for (const t of text.split("\n")) { res.push(h("br")); res.push(t); @@ -104,18 +107,20 @@ export default defineComponent({ } case "italic": { - return h( - "i", - { - style: "font-style: oblique;", - }, - genEl(token.children), - ); + return [ + h( + "i", + { + style: "font-style: oblique;", + }, + genEl(token.children), + ), + ]; } case "fn": { // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる - let style: string; + let style: string | null = null; switch (token.props.name) { case "tada": { const speed = validTime(token.props.args.speed) || "1s"; @@ -188,7 +193,7 @@ export default defineComponent({ if (reducedMotion()) { return genEl(token.children); } - return h(MkSparkle, {}, genEl(token.children)); + return [h(MkSparkle, {}, genEl(token.children))]; } case "fade": { const direction = token.props.args.out @@ -211,31 +216,37 @@ export default defineComponent({ break; } case "x2": { - return h( - "span", - { - class: "mfm-x2", - }, - genEl(token.children), - ); + return [ + h( + "span", + { + class: "mfm-x2", + }, + genEl(token.children), + ), + ]; } case "x3": { - return h( - "span", - { - class: "mfm-x3", - }, - genEl(token.children), - ); + return [ + h( + "span", + { + class: "mfm-x3", + }, + genEl(token.children), + ), + ]; } case "x4": { - return h( - "span", - { - class: "mfm-x4", - }, - genEl(token.children), - ); + return [ + h( + "span", + { + class: "mfm-x4", + }, + genEl(token.children), + ), + ]; } case "font": { const family = token.props.args.serif @@ -255,13 +266,15 @@ export default defineComponent({ break; } case "blur": { - return h( - "span", - { - class: "_blur_text", - }, - genEl(token.children), - ); + return [ + h( + "span", + { + class: "_blur_text", + }, + genEl(token.children), + ), + ]; } case "rotate": { const rotate = token.props.args.x @@ -269,77 +282,105 @@ export default defineComponent({ : token.props.args.y ? "perspective(128px) rotateY" : "rotate"; - const degrees = parseFloat(token.props.args.deg ?? "90"); + const degrees = Number.parseFloat( + token.props.args.deg.toString() ?? "90", + ); style = `transform: ${rotate}(${degrees}deg); transform-origin: center center;`; break; } case "position": { - const x = parseFloat(token.props.args.x ?? "0"); - const y = parseFloat(token.props.args.y ?? "0"); + const x = Number.parseFloat( + token.props.args.x.toString() ?? "0", + ); + const y = Number.parseFloat( + token.props.args.y.toString() ?? "0", + ); style = `transform: translateX(${x}em) translateY(${y}em);`; break; } case "crop": { - const top = parseFloat(token.props.args.top ?? "0"); - const right = parseFloat(token.props.args.right ?? "0"); - const bottom = parseFloat(token.props.args.bottom ?? "0"); - const left = parseFloat(token.props.args.left ?? "0"); + const top = Number.parseFloat( + token.props.args.top.toString() ?? "0", + ); + const right = Number.parseFloat( + token.props.args.right.toString() ?? "0", + ); + const bottom = Number.parseFloat( + token.props.args.bottom.toString() ?? "0", + ); + const left = Number.parseFloat( + token.props.args.left.toString() ?? "0", + ); style = `clip-path: inset(${top}% ${right}% ${bottom}% ${left}%);`; break; } case "scale": { - const x = Math.min(parseFloat(token.props.args.x ?? "1"), 5); - const y = Math.min(parseFloat(token.props.args.y ?? "1"), 5); + const x = Math.min( + Number.parseFloat(token.props.args.x.toString() ?? "1"), + 5, + ); + const y = Math.min( + Number.parseFloat(token.props.args.y.toString() ?? "1"), + 5, + ); style = `transform: scale(${x}, ${y});`; break; } case "fg": { let color = token.props.args.color; - if (!/^[0-9a-f]{3,6}$/i.test(color)) color = "f00"; + if (!/^[0-9a-f]{3,6}$/i.test(color.toString())) color = "f00"; style = `color: #${color};`; break; } case "bg": { let color = token.props.args.color; - if (!/^[0-9a-f]{3,6}$/i.test(color)) color = "f00"; + if (!/^[0-9a-f]{3,6}$/i.test(color.toString())) color = "f00"; style = `background-color: #${color};`; break; } case "small": { - return h( - "small", - { - style: "opacity: 0.7;", - }, - genEl(token.children), - ); + return [ + h( + "small", + { + style: "opacity: 0.7;", + }, + genEl(token.children), + ), + ]; } case "center": { - return h( - "div", - { - style: "text-align: center;", - }, - genEl(token.children), - ); + return [ + h( + "div", + { + style: "text-align: center;", + }, + genEl(token.children), + ), + ]; } } if (style == null) { - return h("span", {}, [ - "$[", - token.props.name, - " ", - ...genEl(token.children), - "]", - ]); + return [ + h("span", {}, [ + "$[", + token.props.name, + " ", + ...genEl(token.children), + "]", + ]), + ]; } else { - return h( - "span", - { - style: `display: inline-block;${style}`, - }, - genEl(token.children), - ); + return [ + h( + "span", + { + style: `display: inline-block;${style}`, + }, + genEl(token.children), + ), + ]; } } @@ -425,7 +466,7 @@ export default defineComponent({ h(MkCode, { key: Math.random(), code: token.props.code, - lang: token.props.lang, + lang: token.props.lang ?? undefined, }), ]; } @@ -506,13 +547,15 @@ export default defineComponent({ const ast2 = (isPlain ? mfm.parseSimple : mfm.parse)( token.props.content.slice(0, -6) + sentinel, ); + function isMfmText(n: mfm.MfmNode): n is mfm.MfmText { + return n.type === "text"; + } + const txtNode = ast2[ast2.length - 1]; if ( - ast2[ast2.length - 1].type === "text" && - ast2[ast2.length - 1].props.text.endsWith(sentinel) + isMfmText(txtNode) && + txtNode.props.text.endsWith(sentinel) ) { - ast2[ast2.length - 1].props.text = ast2[ - ast2.length - 1 - ].props.text.slice(0, -1); + txtNode.props.text = txtNode.props.text.slice(0, -1); } else { // I don't think this scope is reachable console.warn( @@ -554,8 +597,10 @@ export default defineComponent({ } default: { - console.error("unrecognized ast type:", token.type); - + console.error( + "unrecognized ast type:", + (token as { type: never }).type, + ); return []; } } From 23145c61af00349061a312f7f86edce79d037a22 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 17:55:52 +0800 Subject: [PATCH 002/110] refactor: fix type of MkAbuseReport --- packages/backend/src/misc/schema.ts | 2 + .../models/repositories/abuse-user-report.ts | 6 +- .../src/models/schema/abuse-user-report.ts | 69 +++++++++++++++++++ .../api/endpoints/admin/abuse-user-reports.ts | 63 +---------------- .../client/src/components/MkAbuseReport.vue | 5 +- packages/client/src/pages/admin/abuses.vue | 5 +- packages/firefish-js/src/api.types.ts | 14 +++- packages/firefish-js/src/entities.ts | 31 +++++---- 8 files changed, 114 insertions(+), 81 deletions(-) create mode 100644 packages/backend/src/models/schema/abuse-user-report.ts diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index c793d3b2ed..73600832ce 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -33,8 +33,10 @@ import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js"; import { packedEmojiSchema } from "@/models/schema/emoji.js"; import { packedNoteEdit } from "@/models/schema/note-edit.js"; import { packedNoteFileSchema } from "@/models/schema/note-file.js"; +import { packedAbuseUserReportSchema } from "@/models/schema/abuse-user-report.js"; export const refs = { + AbuseUserReport: packedAbuseUserReportSchema, UserLite: packedUserLiteSchema, UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema, MeDetailedOnly: packedMeDetailedOnlySchema, diff --git a/packages/backend/src/models/repositories/abuse-user-report.ts b/packages/backend/src/models/repositories/abuse-user-report.ts index 16ce159955..b8d953d052 100644 --- a/packages/backend/src/models/repositories/abuse-user-report.ts +++ b/packages/backend/src/models/repositories/abuse-user-report.ts @@ -2,6 +2,7 @@ import { db } from "@/db/postgre.js"; import { Users } from "../index.js"; import { AbuseUserReport } from "@/models/entities/abuse-user-report.js"; import { awaitAll } from "@/prelude/await-all.js"; +import type { Packed } from "@/misc/schema.js"; export const AbuseUserReportRepository = db .getRepository(AbuseUserReport) @@ -10,7 +11,7 @@ export const AbuseUserReportRepository = db const report = typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - return await awaitAll({ + const packed: Packed<"AbuseUserReport"> = await awaitAll({ id: report.id, createdAt: report.createdAt.toISOString(), comment: report.comment, @@ -31,9 +32,10 @@ export const AbuseUserReportRepository = db : null, forwarded: report.forwarded, }); + return packed; }, - packMany(reports: any[]) { + packMany(reports: (AbuseUserReport["id"] | AbuseUserReport)[]) { return Promise.all(reports.map((x) => this.pack(x))); }, }); diff --git a/packages/backend/src/models/schema/abuse-user-report.ts b/packages/backend/src/models/schema/abuse-user-report.ts new file mode 100644 index 0000000000..47e56c7415 --- /dev/null +++ b/packages/backend/src/models/schema/abuse-user-report.ts @@ -0,0 +1,69 @@ +export const packedAbuseUserReportSchema = { + type: "object", + properties: { + id: { + type: "string", + optional: false, + nullable: false, + format: "id", + example: "xxxxxxxxxx", + }, + createdAt: { + type: "string", + optional: false, + nullable: false, + format: "date-time", + }, + comment: { + type: "string", + optional: false, + nullable: false, + }, + resolved: { + type: "boolean", + optional: false, + nullable: false, + }, + reporterId: { + type: "string", + optional: false, + nullable: false, + format: "id", + }, + targetUserId: { + type: "string", + optional: false, + nullable: false, + format: "id", + }, + assigneeId: { + type: "string", + optional: false, + nullable: true, + format: "id", + }, + reporter: { + type: "object", + optional: false, + nullable: false, + ref: "UserDetailed", + }, + targetUser: { + type: "object", + optional: false, + nullable: false, + ref: "UserDetailed", + }, + assignee: { + type: "object", + optional: true, + nullable: true, + ref: "UserDetailed", + }, + forwarded: { + type: "boolean", + optional: false, + nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index 78034917f0..4063af5c5c 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -16,68 +16,7 @@ export const meta = { type: "object", optional: false, nullable: false, - properties: { - id: { - type: "string", - nullable: false, - optional: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - nullable: false, - optional: false, - format: "date-time", - }, - comment: { - type: "string", - nullable: false, - optional: false, - }, - resolved: { - type: "boolean", - nullable: false, - optional: false, - example: false, - }, - reporterId: { - type: "string", - nullable: false, - optional: false, - format: "id", - }, - targetUserId: { - type: "string", - nullable: false, - optional: false, - format: "id", - }, - assigneeId: { - type: "string", - nullable: true, - optional: false, - format: "id", - }, - reporter: { - type: "object", - nullable: false, - optional: false, - ref: "User", - }, - targetUser: { - type: "object", - nullable: false, - optional: false, - ref: "User", - }, - assignee: { - type: "object", - nullable: true, - optional: true, - ref: "User", - }, - }, + ref: "AbuseUserReport", }, }, } as const; diff --git a/packages/client/src/components/MkAbuseReport.vue b/packages/client/src/components/MkAbuseReport.vue index 5536523948..26b91fae2e 100644 --- a/packages/client/src/components/MkAbuseReport.vue +++ b/packages/client/src/components/MkAbuseReport.vue @@ -72,13 +72,14 @@ import MkSwitch from "@/components/form/switch.vue"; import MkKeyValue from "@/components/MkKeyValue.vue"; import * as os from "@/os"; import { i18n } from "@/i18n"; +import type { entities } from "firefish-js"; const props = defineProps<{ - report: any; + report: entities.AbuseUserReport; }>(); const emit = defineEmits<{ - (ev: "resolved", reportId: string): void; + resolved: [reportId: string]; }>(); const forward = ref(props.report.forwarded); diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue index 174b4d5713..f94e24cf7c 100644 --- a/packages/client/src/pages/admin/abuses.vue +++ b/packages/client/src/pages/admin/abuses.vue @@ -99,12 +99,13 @@ import XAbuseReport from "@/components/MkAbuseReport.vue"; import { i18n } from "@/i18n"; import { definePageMetadata } from "@/scripts/page-metadata"; import icon from "@/scripts/icon"; +import type { entities } from "firefish-js"; const reports = ref<InstanceType<typeof MkPagination>>(); const state = ref("unresolved"); -const reporterOrigin = ref("combined"); -const targetUserOrigin = ref("combined"); +const reporterOrigin = ref<entities.OriginType>("combined"); +const targetUserOrigin = ref<entities.OriginType>("combined"); // const searchUsername = ref(""); // const searchHost = ref(""); diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts index d3564573f0..81e9a4a72c 100644 --- a/packages/firefish-js/src/api.types.ts +++ b/packages/firefish-js/src/api.types.ts @@ -1,4 +1,5 @@ import type { + AbuseUserReport, Ad, Announcement, Antenna, @@ -68,7 +69,18 @@ type NoteSubmitReq = { export type Endpoints = { // admin - "admin/abuse-user-reports": { req: TODO; res: TODO }; + "admin/abuse-user-reports": { + req: { + limit?: number; + sinceId?: AbuseUserReport["id"]; + untilId?: AbuseUserReport["id"]; + state?: string; + reporterOrigin?: OriginType; + targetUserOrigin?: OriginType; + forwarded?: boolean; + }; + res: AbuseUserReport[]; + }; "admin/delete-all-files-of-a-user": { req: { userId: User["id"] }; res: null; diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index ce940a1481..8501d6c51f 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -18,10 +18,7 @@ export type UserLite = { avatarBlurhash: string; alsoKnownAs: string[]; movedToUri: any; - emojis: { - name: string; - url: string; - }[]; + emojis: EmojiLite[]; instance?: { name: Instance["name"]; softwareName: Instance["softwareName"]; @@ -171,10 +168,7 @@ export type Note = { votes: number; }[]; }; - emojis: { - name: string; - url: string; - }[]; + emojis: EmojiLite[]; uri?: string; url?: string; updatedAt?: DateString; @@ -191,10 +185,7 @@ export type NoteEdit = { updatedAt: string; fileIds: DriveFile["id"][]; files: DriveFile[]; - emojis: { - name: string; - url: string; - }[]; + emojis: EmojiLite[]; }; export type NoteReaction = { @@ -325,6 +316,8 @@ export type EmojiLite = { id: string; name: string; url: string; + width: number | null; + height: number | null; }; export type LiteInstanceMetadata = { @@ -547,3 +540,17 @@ export type UserSorting = | "+updatedAt" | "-updatedAt"; export type OriginType = "combined" | "local" | "remote"; + +export type AbuseUserReport = { + id: string; + createdAt: DateString; + comment: string; + resolved: boolean; + reporterId: User["id"]; + targetUserId: User["id"]; + assigneeId: User["id"] | null; + reporter: UserDetailed; + targetUser: UserDetailed; + assignee?: UserDetailed | null; + forwarded: boolean; +}; From b6baded2e3aabe9217e72b7f7ad2452d3793027b Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 17:58:02 +0800 Subject: [PATCH 003/110] refactor: fix MkAcct type --- packages/client/src/components/global/MkAcct.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/components/global/MkAcct.vue b/packages/client/src/components/global/MkAcct.vue index 02112d4481..dff995f878 100644 --- a/packages/client/src/components/global/MkAcct.vue +++ b/packages/client/src/components/global/MkAcct.vue @@ -16,7 +16,7 @@ import { host as hostRaw } from "@/config"; import { defaultStore } from "@/store"; defineProps<{ - user: entities.UserDetailed; + user: entities.UserLite; detail?: boolean; }>(); From 6ac6a4cfa93c5758c1fed5aace0eb1a036f0e0c0 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 20:10:43 +0800 Subject: [PATCH 004/110] refactor: fix type errors of MkAnnouncements --- packages/client/src/components/MkActiveUsersHeatmap.vue | 8 ++++---- packages/client/src/components/MkAnnouncement.vue | 5 +++-- packages/client/src/components/MkModal.vue | 2 +- packages/firefish-js/src/entities.ts | 2 ++ 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/MkActiveUsersHeatmap.vue b/packages/client/src/components/MkActiveUsersHeatmap.vue index 58eb42f5a6..8099e812f4 100644 --- a/packages/client/src/components/MkActiveUsersHeatmap.vue +++ b/packages/client/src/components/MkActiveUsersHeatmap.vue @@ -18,8 +18,8 @@ import { initChart } from "@/scripts/init-chart"; initChart(); -const rootEl = shallowRef<HTMLDivElement>(); -const chartEl = shallowRef<HTMLCanvasElement>(); +const rootEl = shallowRef<HTMLDivElement | null>(null); +const chartEl = shallowRef<HTMLCanvasElement | null>(null); const now = new Date(); let chartInstance: Chart | null = null; const fetching = ref(true); @@ -33,8 +33,8 @@ async function renderActiveUsersChart() { chartInstance.destroy(); } - const wide = rootEl.value.offsetWidth > 700; - const narrow = rootEl.value.offsetWidth < 400; + const wide = rootEl.value!.offsetWidth > 700; + const narrow = rootEl.value!.offsetWidth < 400; const weeks = wide ? 50 : narrow ? 10 : 25; const chartLimit = 7 * weeks; diff --git a/packages/client/src/components/MkAnnouncement.vue b/packages/client/src/components/MkAnnouncement.vue index 4f26cd8bac..0aaa519cb0 100644 --- a/packages/client/src/components/MkAnnouncement.vue +++ b/packages/client/src/components/MkAnnouncement.vue @@ -35,9 +35,10 @@ import MkSparkle from "@/components/MkSparkle.vue"; import MkButton from "@/components/MkButton.vue"; import { i18n } from "@/i18n"; import * as os from "@/os"; +import type { entities } from "firefish-js"; const props = defineProps<{ - announcement: Announcement; + announcement: entities.Announcement; }>(); const { id, text, title, imageUrl, isGoodNews } = props.announcement; @@ -45,7 +46,7 @@ const { id, text, title, imageUrl, isGoodNews } = props.announcement; const modal = shallowRef<InstanceType<typeof MkModal>>(); const gotIt = () => { - modal.value.close(); + modal.value!.close(); os.api("i/read-announcement", { announcementId: id }); }; </script> diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue index a55525c632..6a2897cebf 100644 --- a/packages/client/src/components/MkModal.vue +++ b/packages/client/src/components/MkModal.vue @@ -190,7 +190,7 @@ const transitionDuration = computed(() => let contentClicking = false; const focusedElement = document.activeElement; -function close(_ev, opts: { useSendAnimation?: boolean } = {}) { +function close(_ev?, opts: { useSendAnimation?: boolean } = {}) { // removeEventListener("popstate", close); // if (props.preferType == "dialog") { // history.forward(); diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index 8501d6c51f..086bf26193 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -422,6 +422,8 @@ export type Announcement = { title: string; imageUrl: string | null; isRead?: boolean; + isGoodNews?: boolean; + showPopUp?: boolean; }; export type Antenna = { From 73537ec6fa3db65bbb72ce17b8fe34514ce06ff2 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 20:36:20 +0800 Subject: [PATCH 005/110] fix type errors of MkAutoComplete --- packages/client/src/components/MkAutocomplete.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/MkAutocomplete.vue b/packages/client/src/components/MkAutocomplete.vue index 9a8582f19a..332785b467 100644 --- a/packages/client/src/components/MkAutocomplete.vue +++ b/packages/client/src/components/MkAutocomplete.vue @@ -62,7 +62,7 @@ <span v-else class="emoji">{{ emoji.emoji }}</span> <span class="name" - v-html="emoji.name.replace(q, `<b>${q}</b>`)" + v-html="q ? emoji.name.replace(q, `<b>${q}</b>`) : emoji.name" ></span> <span v-if="emoji.aliasOf" class="alias" >({{ emoji.aliasOf }})</span @@ -107,7 +107,7 @@ interface EmojiDef { emoji: string; name: string; aliasOf?: string; - url?: string; + url: string; isCustomEmoji?: boolean; } From 1a1d8177720159251716a6f64e51c3f605612610 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 20:37:34 +0800 Subject: [PATCH 006/110] fix type errors of MkAvatars --- packages/client/src/components/MkAvatars.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/MkAvatars.vue b/packages/client/src/components/MkAvatars.vue index d92eee22c1..2b2029a8b5 100644 --- a/packages/client/src/components/MkAvatars.vue +++ b/packages/client/src/components/MkAvatars.vue @@ -9,12 +9,13 @@ <script lang="ts" setup> import { onMounted, ref } from "vue"; import * as os from "@/os"; +import type { entities } from "firefish-js"; const props = defineProps<{ userIds: string[]; }>(); -const users = ref([]); +const users = ref<entities.UserDetailed[]>([]); onMounted(async () => { users.value = await os.api("users/show", { From b59186f09375f337710f3def901fb67b8ed21fcf Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 20:41:31 +0800 Subject: [PATCH 007/110] fix type errors of MkButton --- packages/client/src/components/MkButton.vue | 20 +++++++++++++------ packages/client/src/components/global/MkA.vue | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue index 48864be62a..a0ff747afc 100644 --- a/packages/client/src/components/MkButton.vue +++ b/packages/client/src/components/MkButton.vue @@ -47,7 +47,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (ev: "click", payload: MouseEvent): void; + click: [payload: MouseEvent]; }>(); const el = ref<HTMLElement | null>(null); @@ -61,11 +61,19 @@ onMounted(() => { } }); -function distance(p, q): number { +function distance( + p: { x: number; y: number }, + q: { x: number; y: number }, +): number { return Math.hypot(p.x - q.x, p.y - q.y); } -function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY): number { +function calcCircleScale( + boxW: number, + boxH: number, + circleCenterX: number, + circleCenterY: number, +): number { const origin = { x: circleCenterX, y: circleCenterY }; const dist1 = distance({ x: 0, y: 0 }, origin); const dist2 = distance({ x: boxW, y: 0 }, origin); @@ -79,8 +87,8 @@ function onMousedown(evt: MouseEvent): void { const rect = target.getBoundingClientRect(); const ripple = document.createElement("div"); - ripple.style.top = (evt.clientY - rect.top - 1).toString() + "px"; - ripple.style.left = (evt.clientX - rect.left - 1).toString() + "px"; + ripple.style.top = `${(evt.clientY - rect.top - 1).toString()}px`; + ripple.style.left = `${(evt.clientX - rect.left - 1).toString()}px`; ripples.value!.appendChild(ripple); @@ -97,7 +105,7 @@ function onMousedown(evt: MouseEvent): void { vibrate(10); window.setTimeout(() => { - ripple.style.transform = "scale(" + scale / 2 + ")"; + ripple.style.transform = `scale(${scale / 2})`; }, 1); window.setTimeout(() => { ripple.style.transition = "all 1s ease"; diff --git a/packages/client/src/components/global/MkA.vue b/packages/client/src/components/global/MkA.vue index fbe5472a24..b85774dced 100644 --- a/packages/client/src/components/global/MkA.vue +++ b/packages/client/src/components/global/MkA.vue @@ -22,7 +22,7 @@ import icon from "@/scripts/icon"; const props = withDefaults( defineProps<{ - to: string; + to?: string; activeClass?: null | string; behavior?: null | "window" | "browser" | "modalWindow"; }>(), From c1155f169a79d863aec38a99ef2ab34420d7dc77 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 20:42:48 +0800 Subject: [PATCH 008/110] fix type warnings of MkCaptcha --- packages/client/src/components/MkCaptcha.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/MkCaptcha.vue b/packages/client/src/components/MkCaptcha.vue index 146c512fb8..208f2b627a 100644 --- a/packages/client/src/components/MkCaptcha.vue +++ b/packages/client/src/components/MkCaptcha.vue @@ -50,7 +50,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (ev: "update:modelValue", v: string | null): void; + "update:modelValue": [v: string | null]; }>(); const available = ref(false); @@ -93,6 +93,7 @@ if (loaded) { src: src.value, }), ) + // biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially ).addEventListener("load", () => (available.value = true)); } From f7f7959ba67744919bd4d74fd0580e5cad717925 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 22:24:16 +0800 Subject: [PATCH 009/110] refactor: fix waring for MkCaptcha --- packages/client/src/components/MkCaptcha.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/MkCaptcha.vue b/packages/client/src/components/MkCaptcha.vue index 208f2b627a..135eee8e90 100644 --- a/packages/client/src/components/MkCaptcha.vue +++ b/packages/client/src/components/MkCaptcha.vue @@ -93,8 +93,9 @@ if (loaded) { src: src.value, }), ) - // biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially - ).addEventListener("load", () => (available.value = true)); + ) + // biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially + .addEventListener("load", () => (available.value = true)); } function reset() { From 17a945b8b1558859cb3273a98ecc78bd5e90dc58 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 22:25:11 +0800 Subject: [PATCH 010/110] fix type errors of channel --- .../src/components/MkChannelFollowButton.vue | 5 ++-- .../client/src/components/MkChannelList.vue | 9 ++++--- .../src/components/MkChannelPreview.vue | 3 ++- packages/client/src/components/MkPostForm.vue | 2 +- .../src/components/MkPostFormDialog.vue | 2 +- packages/client/src/pages/channel.vue | 12 +++++---- packages/client/src/pages/channels.vue | 11 ++++---- packages/firefish-js/src/api.types.ts | 27 ++++++++++++++----- packages/firefish-js/src/entities.ts | 11 +++++++- 9 files changed, 56 insertions(+), 26 deletions(-) diff --git a/packages/client/src/components/MkChannelFollowButton.vue b/packages/client/src/components/MkChannelFollowButton.vue index c1910bc595..98e239195c 100644 --- a/packages/client/src/components/MkChannelFollowButton.vue +++ b/packages/client/src/components/MkChannelFollowButton.vue @@ -27,10 +27,11 @@ import { ref } from "vue"; import * as os from "@/os"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; +import type { entities } from "firefish-js"; const props = withDefaults( defineProps<{ - channel: Record<string, any>; + channel: entities.Channel; full?: boolean; }>(), { @@ -38,7 +39,7 @@ const props = withDefaults( }, ); -const isFollowing = ref<boolean>(props.channel.isFollowing); +const isFollowing = ref<boolean>(props.channel.isFollowing ?? false); const wait = ref(false); async function onClick() { diff --git a/packages/client/src/components/MkChannelList.vue b/packages/client/src/components/MkChannelList.vue index 15f199c90f..ce2b7790d4 100644 --- a/packages/client/src/components/MkChannelList.vue +++ b/packages/client/src/components/MkChannelList.vue @@ -11,7 +11,7 @@ </div> </template> - <template #default="{ items }"> + <template #default="{ items }: { items: entities.Channel[] }"> <MkChannelPreview v-for="item in items" :key="item.id" @@ -29,14 +29,15 @@ import MkPagination from "@/components/MkPagination.vue"; import { i18n } from "@/i18n"; import type { entities } from "firefish-js"; -const props = withDefaults( +withDefaults( defineProps<{ pagination: PagingOf<entities.Channel>; noGap?: boolean; - extractor?: (item: any) => any; + // TODO: this function is not used and may can be removed + extractor?: (item: entities.Channel) => entities.Channel; }>(), { - extractor: (item) => item, + extractor: (item: entities.Channel) => item, }, ); </script> diff --git a/packages/client/src/components/MkChannelPreview.vue b/packages/client/src/components/MkChannelPreview.vue index f824a1b2f5..8b2e12dc8b 100644 --- a/packages/client/src/components/MkChannelPreview.vue +++ b/packages/client/src/components/MkChannelPreview.vue @@ -54,9 +54,10 @@ import { computed } from "vue"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; +import type { entities } from "firefish-js"; const props = defineProps<{ - channel: Record<string, any>; + channel: entities.Channel; }>(); const bannerStyle = computed(() => { diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue index ba58ea652a..3254591bc3 100644 --- a/packages/client/src/components/MkPostForm.vue +++ b/packages/client/src/components/MkPostForm.vue @@ -354,7 +354,7 @@ const props = withDefaults( defineProps<{ reply?: entities.Note; renote?: entities.Note; - channel?: any; // TODO + channel?: entities.Channel; mention?: entities.User; specified?: entities.User; initialText?: string; diff --git a/packages/client/src/components/MkPostFormDialog.vue b/packages/client/src/components/MkPostFormDialog.vue index 60637ff237..5ae9e520d4 100644 --- a/packages/client/src/components/MkPostFormDialog.vue +++ b/packages/client/src/components/MkPostFormDialog.vue @@ -28,7 +28,7 @@ import MkPostForm from "@/components/MkPostForm.vue"; const props = defineProps<{ reply?: entities.Note; renote?: entities.Note; - channel?: any; // TODO + channel?: entities.Channel; mention?: entities.User; specified?: entities.User; initialText?: string; diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue index 15e8b6e256..b8ec491ecb 100644 --- a/packages/client/src/pages/channel.vue +++ b/packages/client/src/pages/channel.vue @@ -33,7 +33,7 @@ :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` - : null, + : undefined, }" class="banner" > @@ -88,8 +88,6 @@ class="_gap" src="channel" :channel="channelId" - @before="before" - @after="after" /> </div> </MkSpacer> @@ -107,6 +105,7 @@ import { me } from "@/me"; import { i18n } from "@/i18n"; import { definePageMetadata } from "@/scripts/page-metadata"; import icon from "@/scripts/icon"; +import type { entities } from "firefish-js"; const router = useRouter(); @@ -114,7 +113,11 @@ const props = defineProps<{ channelId: string; }>(); -const channel = ref(null); +const channel = ref<entities.Channel>( + await os.api("channels/show", { + channelId: props.channelId, + }), +); const showBanner = ref(true); watch( @@ -124,7 +127,6 @@ watch( channelId: props.channelId, }); }, - { immediate: true }, ); function edit() { diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue index ec92af54aa..5286f909d4 100644 --- a/packages/client/src/pages/channels.vue +++ b/packages/client/src/pages/channels.vue @@ -125,6 +125,7 @@ import { defaultStore } from "@/store"; import icon from "@/scripts/icon"; import "swiper/scss"; import "swiper/scss/virtual"; +import type { Swiper as SwiperType } from "swiper/types"; const router = useRouter(); @@ -216,18 +217,18 @@ definePageMetadata( })), ); -let swiperRef = null; +let swiperRef: SwiperType | null = null; -function setSwiperRef(swiper) { +function setSwiperRef(swiper: SwiperType) { swiperRef = swiper; syncSlide(tabs.indexOf(tab.value)); } function onSlideChange() { - tab.value = tabs[swiperRef.activeIndex]; + tab.value = tabs[swiperRef!.activeIndex]; } -function syncSlide(index) { - swiperRef.slideTo(index); +function syncSlide(index: number) { + swiperRef!.slideTo(index); } </script> diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts index 81e9a4a72c..9d1123e7ad 100644 --- a/packages/firefish-js/src/api.types.ts +++ b/packages/firefish-js/src/api.types.ts @@ -218,16 +218,31 @@ export type Endpoints = { }; // channels - "channels/create": { req: TODO; res: TODO }; - "channels/featured": { req: TODO; res: TODO }; + "channels/create": { + req: { + name: string; + description?: string; + bannerId: DriveFile["id"] | null; + }; + res: Channel; + }; + "channels/featured": { req: TODO; res: Channel[] }; "channels/follow": { req: TODO; res: TODO }; - "channels/followed": { req: TODO; res: TODO }; - "channels/owned": { req: TODO; res: TODO }; + "channels/followed": { req: TODO; res: Channel[] }; + "channels/owned": { req: TODO; res: Channel[] }; "channels/pin-note": { req: TODO; res: TODO }; - "channels/show": { req: TODO; res: TODO }; + "channels/show": { req: TODO; res: Channel }; "channels/timeline": { req: TODO; res: Note[] }; "channels/unfollow": { req: TODO; res: TODO }; - "channels/update": { req: TODO; res: TODO }; + "channels/update": { + req: { + channelId: Channel["id"]; + name: string; + description?: string; + bannerId: DriveFile["id"] | null; + }; + res: Channel; + }; // charts "charts/active-users": { diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index 086bf26193..74e97f2781 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -471,8 +471,17 @@ export type FollowRequest = { export type Channel = { id: ID; + createdAt: DateString; + lastNotedAt: DateString | null; name: string; - // TODO + description: string | null; + bannerId: DriveFile["id"]; + bannerUrl: string | null; + notesCount: number; + usersCount: number; + isFollowing?: boolean; + userId: User["id"] | null; + hasUnreadNote?: boolean; }; export type Following = { From 69828437161751568ec291fc77cb07d9583c9585 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 22:25:45 +0800 Subject: [PATCH 011/110] fix: channel editor cannot remove channel banner --- .../src/models/repositories/channel.ts | 1 + packages/backend/src/models/schema/channel.ts | 12 +++ .../server/api/endpoints/channels/update.ts | 2 +- packages/client/src/pages/channel-editor.vue | 80 ++++++++++++------- 4 files changed, 67 insertions(+), 28 deletions(-) diff --git a/packages/backend/src/models/repositories/channel.ts b/packages/backend/src/models/repositories/channel.ts index 857470f4ec..809129db6c 100644 --- a/packages/backend/src/models/repositories/channel.ts +++ b/packages/backend/src/models/repositories/channel.ts @@ -40,6 +40,7 @@ export const ChannelRepository = db.getRepository(Channel).extend({ name: channel.name, description: channel.description, userId: channel.userId, + bannerId: channel.bannerId, bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null, usersCount: channel.usersCount, notesCount: channel.notesCount, diff --git a/packages/backend/src/models/schema/channel.ts b/packages/backend/src/models/schema/channel.ts index 67833cb0dd..d3ec222c8d 100644 --- a/packages/backend/src/models/schema/channel.ts +++ b/packages/backend/src/models/schema/channel.ts @@ -36,6 +36,13 @@ export const packedChannelSchema = { nullable: true, optional: false, }, + bannerId: { + type: "string", + optional: false, + nullable: true, + format: "id", + example: "xxxxxxxxxx", + }, notesCount: { type: "number", nullable: false, @@ -57,5 +64,10 @@ export const packedChannelSchema = { optional: false, format: "id", }, + hasUnreadNote: { + type: "boolean", + optional: true, + nullable: false, + }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index 0de7a837a1..fdd21da65f 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -83,7 +83,7 @@ export default define(meta, paramDef, async (ps, me) => { await Channels.update(channel.id, { ...(ps.name !== undefined ? { name: ps.name } : {}), ...(ps.description !== undefined ? { description: ps.description } : {}), - ...(banner ? { bannerId: banner.id } : {}), + ...(banner ? { bannerId: banner.id } : { bannerId: null }), }); return await Channels.pack(channel.id, me); diff --git a/packages/client/src/pages/channel-editor.vue b/packages/client/src/pages/channel-editor.vue index 60bcfe990e..3791a98d05 100644 --- a/packages/client/src/pages/channel-editor.vue +++ b/packages/client/src/pages/channel-editor.vue @@ -50,6 +50,7 @@ import { useRouter } from "@/router"; import { definePageMetadata } from "@/scripts/page-metadata"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; +import type { entities } from "firefish-js"; const router = useRouter(); @@ -57,26 +58,24 @@ const props = defineProps<{ channelId?: string; }>(); -const channel = ref(null); -const name = ref(null); -const description = ref(null); +const channel = ref<entities.Channel | null>(null); +const name = ref<string>(""); +const description = ref<string>(""); const bannerUrl = ref<string | null>(null); const bannerId = ref<string | null>(null); -watch( - () => bannerId.value, - async () => { - if (bannerId.value == null) { - bannerUrl.value = null; - } else { - bannerUrl.value = ( - await os.api("drive/files/show", { - fileId: bannerId.value, - }) - ).url; - } - }, -); +let bannerUrlUpdated = false; + +/** + * Set banner url and id when we already know the url + * Prevent redundant network requests from being sent + */ +function setBanner(opt: { bannerId: string | null; bannerUrl: string | null }) { + bannerUrlUpdated = true; + bannerUrl.value = opt.bannerUrl; + bannerId.value = opt.bannerId; + bannerUrlUpdated = false; +} async function fetchChannel() { if (props.channelId == null) return; @@ -86,23 +85,44 @@ async function fetchChannel() { }); name.value = channel.value.name; - description.value = channel.value.description; - bannerId.value = channel.value.bannerId; - bannerUrl.value = channel.value.bannerUrl; + description.value = channel.value.description ?? ""; + setBanner(channel.value); } -fetchChannel(); +await fetchChannel(); + +watch(bannerId, async () => { + if (bannerUrlUpdated) { + bannerUrlUpdated = false; + return; + } + if (bannerId.value == null) { + bannerUrl.value = null; + } else { + bannerUrl.value = ( + await os.api("drive/files/show", { + fileId: bannerId.value, + }) + ).url; + } +}); function save() { - const params = { + const params: { + name: string; + description: string; + bannerId: string | null; + } = { name: name.value, description: description.value, bannerId: bannerId.value, }; if (props.channelId) { - params.channelId = props.channelId; - os.api("channels/update", params).then(() => { + os.api("channels/update", { + ...params, + channelId: props.channelId, + }).then(() => { os.success(); }); } else { @@ -113,14 +133,20 @@ function save() { } } -function setBannerImage(evt) { +function setBannerImage(evt: MouseEvent) { selectFile(evt.currentTarget ?? evt.target, null).then((file) => { - bannerId.value = file.id; + setBanner({ + bannerId: file.id, + bannerUrl: file.url, + }); }); } function removeBannerImage() { - bannerId.value = null; + setBanner({ + bannerId: null, + bannerUrl: null, + }); } const headerActions = computed(() => []); From 0b7ab1b90dc97e94007a5c17251c921fc9282019 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 22:45:01 +0800 Subject: [PATCH 012/110] fix type errors of MkChatPreview --- .../client/src/components/MkChatPreview.vue | 24 +++++++++++-------- packages/firefish-js/src/entities.ts | 4 +++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/client/src/components/MkChatPreview.vue b/packages/client/src/components/MkChatPreview.vue index 1b330b7b2d..6976a3f18a 100644 --- a/packages/client/src/components/MkChatPreview.vue +++ b/packages/client/src/components/MkChatPreview.vue @@ -4,14 +4,14 @@ :class="{ isMe: isMe(message), isRead: message.groupId - ? message.reads.includes(me?.id) + ? message.reads.includes(me!.id) : message.isRead, }" :to=" message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${acct.toString( - isMe(message) ? message.recipient : message.user, + isMe(message) ? message.recipient! : message.user, )}` " > @@ -22,27 +22,27 @@ message.groupId ? message.user : isMe(message) - ? message.recipient + ? message.recipient! : message.user " :show-indicator="true" disable-link /> <header v-if="message.groupId"> - <span class="name">{{ message.group.name }}</span> + <span class="name">{{ message.group!.name }}</span> <MkTime :time="message.createdAt" class="time" /> </header> <header v-else> <span class="name" ><MkUserName :user=" - isMe(message) ? message.recipient : message.user + isMe(message) ? message.recipient! : message.user " /></span> <span class="username" >@{{ acct.toString( - isMe(message) ? message.recipient : message.user, + isMe(message) ? message.recipient! : message.user, ) }}</span > @@ -65,16 +65,20 @@ </template> <script lang="ts" setup> -import { acct } from "firefish-js"; +import { acct, type entities } from "firefish-js"; import { i18n } from "@/i18n"; import { me } from "@/me"; +if (me == null) { + throw "No me"; +} + defineProps<{ - message: Record<string, any>; + message: entities.MessagingMessage; }>(); -function isMe(message): boolean { - return message.userId === me?.id; +function isMe(message: entities.MessagingMessage): boolean { + return message.userId === me!.id; } </script> diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index 74e97f2781..16cef32827 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -76,7 +76,9 @@ export type UserDetailed = UserLite & { url: string | null; }; -export type UserGroup = TODO; +export type UserGroup = { + id: ID; +} & Record<string, TODO>; export type UserList = { id: ID; From 124b2244d62b259dc20f103bd2004cfd94ada76d Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 22:50:30 +0800 Subject: [PATCH 013/110] fix type errors of MkCode & MkModalWindow --- packages/client/src/components/MkCode.core.vue | 1 + packages/client/src/components/MkModalWindow.vue | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/MkCode.core.vue b/packages/client/src/components/MkCode.core.vue index 11720b4f62..7e3cdac562 100644 --- a/packages/client/src/components/MkCode.core.vue +++ b/packages/client/src/components/MkCode.core.vue @@ -29,6 +29,7 @@ if (props.lang != null && !(props.lang in Prism.languages)) { const { lang } = props; loadLanguage(props.lang).then( // onLoaded + // biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally () => (prismLang.value = lang), // onError () => {}, diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue index b7ba30dd17..3d04ce00de 100644 --- a/packages/client/src/components/MkModalWindow.vue +++ b/packages/client/src/components/MkModalWindow.vue @@ -97,7 +97,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>(); const rootEl = shallowRef<HTMLElement>(); const headerEl = shallowRef<HTMLElement>(); -const close = (ev) => { +const close = (ev?) => { modal.value?.close(ev); }; From 909125e519829b59e06c2a2f4213a9d521877f9f Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 23:35:14 +0800 Subject: [PATCH 014/110] refactor: rewrite MkContainer using composition api --- .../client/src/components/MkContainer.vue | 201 ++++++++---------- 1 file changed, 92 insertions(+), 109 deletions(-) diff --git a/packages/client/src/components/MkContainer.vue b/packages/client/src/components/MkContainer.vue index 404f984415..6d91b39fca 100644 --- a/packages/client/src/components/MkContainer.vue +++ b/packages/client/src/components/MkContainer.vue @@ -9,6 +9,7 @@ scrollable, closed: !showBody, }" + ref="el" > <header v-if="showHeader" ref="header"> <div class="title"><slot name="header"></slot></div> @@ -59,123 +60,105 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from "vue"; +<script lang="ts" setup> +import { onMounted, ref, watch } from "vue"; import { i18n } from "@/i18n"; import { defaultStore } from "@/store"; import icon from "@/scripts/icon"; -export default defineComponent({ - props: { - showHeader: { - type: Boolean, - required: false, - default: true, - }, - thin: { - type: Boolean, - required: false, - default: false, - }, - naked: { - type: Boolean, - required: false, - default: false, - }, - foldable: { - type: Boolean, - required: false, - default: false, - }, - expanded: { - type: Boolean, - required: false, - default: true, - }, - scrollable: { - type: Boolean, - required: false, - default: false, - }, - maxHeight: { - type: Number, - required: false, - default: null, - }, - }, - data() { - return { - showBody: this.expanded, - omitted: null, - ignoreOmit: false, - i18n, - icon, - defaultStore, - }; - }, - mounted() { - this.$watch( - "showBody", - (showBody) => { - const headerHeight = this.showHeader - ? this.$refs.header.offsetHeight - : 0; - this.$el.style.minHeight = `${headerHeight}px`; - if (showBody) { - this.$el.style.flexBasis = "auto"; - } else { - this.$el.style.flexBasis = `${headerHeight}px`; - } - }, - { - immediate: true, - }, - ); +const props = withDefaults( + defineProps<{ + showHeader?: boolean, + thin?: boolean, + naked?: boolean, + foldable?: boolean, + expanded?: boolean, + scrollable?: boolean, + maxHeight?: number | null, + }>(), + { + showHeader: true, + thin: false, + naked: false, + foldable: false, + expanded: true, + scrollable: false, + maxHeight: null, + } +); - this.$el.style.setProperty("--maxHeight", this.maxHeight + "px"); +const showBody = ref(props.expanded); +const omitted = ref<boolean | null>(null); +const ignoreOmit = ref(false); +const el = ref<HTMLElement | null>(null); +const header = ref<HTMLElement | null>(null); +const content = ref<HTMLElement | null>(null); - const calcOmit = () => { - if ( - this.omitted || - this.ignoreOmit || - this.maxHeight == null || - this.$refs.content == null - ) - return; - const height = this.$refs.content.offsetHeight; - this.omitted = height > this.maxHeight; - }; +// FIXME: This function is not used, why? +function toggleContent(show: boolean) { + if (!props.foldable) return; + showBody.value = show; +} +function enter(el) { + const elementHeight = el.getBoundingClientRect().height; + el.style.height = 0; + el.offsetHeight; // reflow + el.style.height = `${elementHeight}px`; +} +function afterEnter(el) { + el.style.height = null; +} +function leave(el) { + const elementHeight = el.getBoundingClientRect().height; + el.style.height = `${elementHeight}px`; + el.offsetHeight; // reflow + el.style.height = 0; +} +function afterLeave(el) { + el.style.height = null; +} + +onMounted(() => { + watch( + showBody, + (showBody) => { + const headerHeight = props.showHeader + ? header.value!.offsetHeight + : 0; + el.value!.style.minHeight = `${headerHeight}px`; + if (showBody) { + el.value!.style.flexBasis = "auto"; + } else { + el.value!.style.flexBasis = `${headerHeight}px`; + } + }, + { + immediate: true, + } + ); + + if (props.maxHeight != null) { + el.value!.style.setProperty("--maxHeight", `${props.maxHeight}px`); + } + + const calcOmit = () => { + if ( + omitted.value || + ignoreOmit.value || + props.maxHeight == null || + content.value == null + ) + return; + const height = content.value.offsetHeight; + omitted.value = height > props.maxHeight; + }; + + calcOmit(); + + new ResizeObserver((_entries, _observer) => { calcOmit(); - new ResizeObserver((entries, observer) => { - calcOmit(); - }).observe(this.$refs.content); - }, - methods: { - toggleContent(show: boolean) { - if (!this.foldable) return; - this.showBody = show; - }, - - enter(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = 0; - el.offsetHeight; // reflow - el.style.height = elementHeight + "px"; - }, - afterEnter(el) { - el.style.height = null; - }, - leave(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + "px"; - el.offsetHeight; // reflow - el.style.height = 0; - }, - afterLeave(el) { - el.style.height = null; - }, - }, + }).observe(content.value!); }); </script> From 43aeec32ce1c8d3c49793cdf8bcffce6663a0d40 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 10 Apr 2024 23:36:42 +0800 Subject: [PATCH 015/110] fix types error of MkContextMenu --- packages/client/src/components/MkContextMenu.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/client/src/components/MkContextMenu.vue b/packages/client/src/components/MkContextMenu.vue index 319f7fd0fe..86ca3b3f3e 100644 --- a/packages/client/src/components/MkContextMenu.vue +++ b/packages/client/src/components/MkContextMenu.vue @@ -28,7 +28,7 @@ const emit = defineEmits<{ (ev: "closed"): void; }>(); -const rootEl = ref<HTMLDivElement>(); +const rootEl = ref<HTMLDivElement | null>(null); const zIndex = ref<number>(os.claimZIndex("high")); @@ -36,8 +36,8 @@ onMounted(() => { let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 - const width = rootEl.value.offsetWidth; - const height = rootEl.value.offsetHeight; + const width = rootEl.value!.offsetWidth; + const height = rootEl.value!.offsetHeight; if (left + width - window.scrollX > window.innerWidth) { left = window.innerWidth - width + window.scrollX; @@ -55,8 +55,8 @@ onMounted(() => { left = 0; } - rootEl.value.style.top = `${top}px`; - rootEl.value.style.left = `${left}px`; + rootEl.value!.style.top = `${top}px`; + rootEl.value!.style.left = `${left}px`; document.body.addEventListener("mousedown", onMousedown); }); From 18ba024cbb06f24406ad207be6b933f96bf87980 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Thu, 11 Apr 2024 00:00:54 +0800 Subject: [PATCH 016/110] chore: format --- .../client/src/components/MkContainer.vue | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/client/src/components/MkContainer.vue b/packages/client/src/components/MkContainer.vue index 6d91b39fca..dfc5ec26ba 100644 --- a/packages/client/src/components/MkContainer.vue +++ b/packages/client/src/components/MkContainer.vue @@ -68,13 +68,13 @@ import icon from "@/scripts/icon"; const props = withDefaults( defineProps<{ - showHeader?: boolean, - thin?: boolean, - naked?: boolean, - foldable?: boolean, - expanded?: boolean, - scrollable?: boolean, - maxHeight?: number | null, + showHeader?: boolean; + thin?: boolean; + naked?: boolean; + foldable?: boolean; + expanded?: boolean; + scrollable?: boolean; + maxHeight?: number | null; }>(), { showHeader: true, @@ -84,7 +84,7 @@ const props = withDefaults( expanded: true, scrollable: false, maxHeight: null, - } + }, ); const showBody = ref(props.expanded); @@ -123,9 +123,7 @@ onMounted(() => { watch( showBody, (showBody) => { - const headerHeight = props.showHeader - ? header.value!.offsetHeight - : 0; + const headerHeight = props.showHeader ? header.value!.offsetHeight : 0; el.value!.style.minHeight = `${headerHeight}px`; if (showBody) { el.value!.style.flexBasis = "auto"; @@ -135,7 +133,7 @@ onMounted(() => { }, { immediate: true, - } + }, ); if (props.maxHeight != null) { From f275fc9cdf0e1d80c4528a6cf13f525cd3930b22 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Thu, 11 Apr 2024 00:14:36 +0800 Subject: [PATCH 017/110] refactor: fix type errors of MkCropperDialog --- .../client/src/components/MkCropperDialog.vue | 56 +++++++++++-------- .../client/src/components/MkModalWindow.vue | 5 +- packages/client/src/os.ts | 8 +-- 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/packages/client/src/components/MkCropperDialog.vue b/packages/client/src/components/MkCropperDialog.vue index 16b42c2f2a..955481bdad 100644 --- a/packages/client/src/components/MkCropperDialog.vue +++ b/packages/client/src/components/MkCropperDialog.vue @@ -68,40 +68,48 @@ let cropper: Cropper | null = null; const loading = ref(true); const ok = async () => { - const promise = new Promise<entities.DriveFile>(async (res) => { + async function UploadCroppedImg(): Promise<entities.DriveFile> { const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas(); - croppedCanvas.toBlob((blob) => { - const formData = new FormData(); - formData.append("file", blob); - if (defaultStore.state.uploadFolder) { - formData.append("folderId", defaultStore.state.uploadFolder); - } - fetch(apiUrl + "/drive/files/create", { - method: "POST", - body: formData, - headers: { - authorization: `Bearer ${me.token}`, - }, - }) - .then((response) => response.json()) - .then((f) => { - res(f); - }); + const blob = await new Promise<Blob | null>((resolve) => + croppedCanvas!.toBlob((blob) => resolve(blob)), + ); + + // MDN says `null` may be passed if the image cannot be created for any reason. + // But I don't think this is reachable for normal case. + if (blob == null) { + throw "Cropping image failed."; + } + + const formData = new FormData(); + formData.append("file", blob); + if (defaultStore.state.uploadFolder) { + formData.append("folderId", defaultStore.state.uploadFolder); + } + + const response = await fetch(`${apiUrl}/drive/files/create`, { + method: "POST", + body: formData, + headers: { + authorization: `Bearer ${me!.token}`, + }, }); - }); + return await response.json(); + } + + const promise = UploadCroppedImg(); os.promiseDialog(promise); const f = await promise; emit("ok", f); - dialogEl.value.close(); + dialogEl.value!.close(); }; const cancel = () => { emit("cancel"); - dialogEl.value.close(); + dialogEl.value!.close(); }; const onImageLoad = () => { @@ -114,7 +122,7 @@ const onImageLoad = () => { }; onMounted(() => { - cropper = new Cropper(imgEl.value, {}); + cropper = new Cropper(imgEl.value!, {}); const computedStyle = getComputedStyle(document.documentElement); @@ -127,13 +135,13 @@ onMounted(() => { selection.outlined = true; window.setTimeout(() => { - cropper.getCropperImage()!.$center("contain"); + cropper!.getCropperImage()!.$center("contain"); selection.$center(); }, 100); // モーダルオープンアニメーションが終わったあとで再度調整 window.setTimeout(() => { - cropper.getCropperImage()!.$center("contain"); + cropper!.getCropperImage()!.$center("contain"); selection.$center(); }, 500); }); diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue index 3d04ce00de..c9d126ee90 100644 --- a/packages/client/src/components/MkModalWindow.vue +++ b/packages/client/src/components/MkModalWindow.vue @@ -54,7 +54,10 @@ </button> </div> <div class="body"> - <slot></slot> + <slot + :width="width" + :height="height" + ></slot> </div> </div> </FocusTrap> diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index b8fac741ea..328b961b21 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -125,12 +125,12 @@ export const apiWithDialog = (( return promise; }) as typeof api; -export function promiseDialog<T extends Promise<any>>( - promise: T, - onSuccess?: ((res: any) => void) | null, +export function promiseDialog<T>( + promise: Promise<T>, + onSuccess?: ((res: T) => void) | null, onFailure?: ((err: Error) => void) | null, text?: string, -): T { +): Promise<T> { const showing = ref(true); const success = ref(false); From 3ddd68097abcc2416d1e42cfa2c11ec497789ac4 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Thu, 11 Apr 2024 00:48:35 +0800 Subject: [PATCH 018/110] fix type errors --- packages/client/src/components/MkCwButton.vue | 2 +- packages/client/src/components/MkDigitalClock.vue | 2 +- packages/client/src/components/MkDonation.vue | 7 ++++--- packages/client/src/components/MkDrive.file.vue | 3 ++- packages/client/src/components/MkEmojiPicker.section.vue | 2 +- packages/firefish-js/src/entities.ts | 1 + 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue index e7a2d3d77a..ccdea3ec90 100644 --- a/packages/client/src/components/MkCwButton.vue +++ b/packages/client/src/components/MkCwButton.vue @@ -48,7 +48,7 @@ const toggle = () => { }; function focus() { - el.value.focus(); + el.value?.focus(); } defineExpose({ diff --git a/packages/client/src/components/MkDigitalClock.vue b/packages/client/src/components/MkDigitalClock.vue index b42ecf19eb..42eabdf749 100644 --- a/packages/client/src/components/MkDigitalClock.vue +++ b/packages/client/src/components/MkDigitalClock.vue @@ -26,7 +26,7 @@ const props = withDefaults( }, ); -let intervalId; +let intervalId: number; const hh = ref(""); const mm = ref(""); const ss = ref(""); diff --git a/packages/client/src/components/MkDonation.vue b/packages/client/src/components/MkDonation.vue index 1c13754c13..ad9df629f4 100644 --- a/packages/client/src/components/MkDonation.vue +++ b/packages/client/src/components/MkDonation.vue @@ -29,7 +29,7 @@ <MkButton v-if="instance.donationLink" gradate - @click="openExternal(instance.donationLink)" + @click="openExternal(instance.donationLink!)" >{{ i18n.t("_aboutFirefish.donateHost", { host: hostname, @@ -73,7 +73,8 @@ const emit = defineEmits<{ (ev: "closed"): void; }>(); -const hostname = instance.name?.length < 38 ? instance.name : host; +const hostname = + instance.name?.length && instance.name?.length < 38 ? instance.name : host; const zIndex = os.claimZIndex("low"); @@ -97,7 +98,7 @@ function neverShow() { close(); } -function openExternal(link) { +function openExternal(link: string) { window.open(link, "_blank"); } </script> diff --git a/packages/client/src/components/MkDrive.file.vue b/packages/client/src/components/MkDrive.file.vue index 5e8a50eec2..9d09da663e 100644 --- a/packages/client/src/components/MkDrive.file.vue +++ b/packages/client/src/components/MkDrive.file.vue @@ -47,6 +47,7 @@ import * as os from "@/os"; import { i18n } from "@/i18n"; import { me } from "@/me"; import icon from "@/scripts/icon"; +import type { MenuItem } from "@/types/menu"; const props = withDefaults( defineProps<{ @@ -72,7 +73,7 @@ const title = computed( () => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`, ); -function getMenu() { +function getMenu(): MenuItem[] { return [ { text: i18n.ts.rename, diff --git a/packages/client/src/components/MkEmojiPicker.section.vue b/packages/client/src/components/MkEmojiPicker.section.vue index b91195fe65..9fcd5d363d 100644 --- a/packages/client/src/components/MkEmojiPicker.section.vue +++ b/packages/client/src/components/MkEmojiPicker.section.vue @@ -14,7 +14,7 @@ class="_button" @click.stop=" applyUnicodeSkinTone( - props.skinTones.indexOf(skinTone) + 1, + props.skinTones!.indexOf(skinTone) + 1, ) " > diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index 16cef32827..387f4eb608 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -329,6 +329,7 @@ export type LiteInstanceMetadata = { name: string | null; uri: string; description: string | null; + donationLink?: string; tosUrl: string | null; disableRegistration: boolean; disableLocalTimeline: boolean; From 783f5481bb762043aed1cec90355000e4ba6bc4a Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Thu, 11 Apr 2024 16:58:58 +0800 Subject: [PATCH 019/110] remove unnecessary assertion --- packages/client/src/components/MkChatPreview.vue | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/client/src/components/MkChatPreview.vue b/packages/client/src/components/MkChatPreview.vue index 6976a3f18a..62235b9a22 100644 --- a/packages/client/src/components/MkChatPreview.vue +++ b/packages/client/src/components/MkChatPreview.vue @@ -69,10 +69,6 @@ import { acct, type entities } from "firefish-js"; import { i18n } from "@/i18n"; import { me } from "@/me"; -if (me == null) { - throw "No me"; -} - defineProps<{ message: entities.MessagingMessage; }>(); From 7a4e6334f16c73edff97f7cbd225e26c85bbd881 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Thu, 11 Apr 2024 17:01:37 +0800 Subject: [PATCH 020/110] fix type error --- packages/firefish-js/src/entities.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index 387f4eb608..cafbdc7864 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -425,8 +425,8 @@ export type Announcement = { title: string; imageUrl: string | null; isRead?: boolean; - isGoodNews?: boolean; - showPopUp?: boolean; + isGoodNews: boolean; + showPopUp: boolean; }; export type Antenna = { From 3716e7f74cde6e1aa2753074eecfc37715214277 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Thu, 11 Apr 2024 17:11:48 +0800 Subject: [PATCH 021/110] fix: expose toggleContent for it was a method --- packages/client/src/components/MkContainer.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/MkContainer.vue b/packages/client/src/components/MkContainer.vue index dfc5ec26ba..e933b02dbf 100644 --- a/packages/client/src/components/MkContainer.vue +++ b/packages/client/src/components/MkContainer.vue @@ -94,7 +94,6 @@ const el = ref<HTMLElement | null>(null); const header = ref<HTMLElement | null>(null); const content = ref<HTMLElement | null>(null); -// FIXME: This function is not used, why? function toggleContent(show: boolean) { if (!props.foldable) return; showBody.value = show; @@ -158,6 +157,14 @@ onMounted(() => { calcOmit(); }).observe(content.value!); }); + +defineExpose({ + toggleContent, + enter, + afterEnter, + leave, + afterLeave, +}); </script> <style lang="scss" scoped> From db37eb4ad123cfe1352a27d640b8231750bfb296 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Thu, 11 Apr 2024 17:29:07 +0800 Subject: [PATCH 022/110] refactor: rewrite MkFolder --- packages/client/src/components/MkFolder.vue | 120 ++++++++++---------- 1 file changed, 62 insertions(+), 58 deletions(-) diff --git a/packages/client/src/components/MkFolder.vue b/packages/client/src/components/MkFolder.vue index 8f545406d1..1e69996054 100644 --- a/packages/client/src/components/MkFolder.vue +++ b/packages/client/src/components/MkFolder.vue @@ -31,72 +31,76 @@ </section> </template> -<script lang="ts"> -import { defineComponent } from "vue"; +<script lang="ts" setup> +import { ref, watch } from "vue"; import { getUniqueId } from "@/os"; import { defaultStore } from "@/store"; // import icon from "@/scripts/icon"; const localStoragePrefix = "ui:folder:"; -export default defineComponent({ - props: { - expanded: { - type: Boolean, - required: false, - default: true, - }, - persistKey: { - type: String, - required: false, - default: null, - }, +const props = withDefaults( + defineProps<{ + expanded?: boolean; + persistKey?: string | null; + }>(), + { + expanded: true, + persistKey: null, }, - data() { - return { - bodyId: getUniqueId(), - showBody: - this.persistKey && - localStorage.getItem(localStoragePrefix + this.persistKey) - ? localStorage.getItem(localStoragePrefix + this.persistKey) === "t" - : this.expanded, - animation: defaultStore.state.animation, - }; - }, - watch: { - showBody() { - if (this.persistKey) { - localStorage.setItem( - localStoragePrefix + this.persistKey, - this.showBody ? "t" : "f", - ); - } - }, - }, - methods: { - toggleContent(show: boolean) { - this.showBody = show; - }, +); - enter(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = 0; - el.offsetHeight; // reflow - el.style.height = elementHeight + "px"; - }, - afterEnter(el) { - el.style.height = null; - }, - leave(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + "px"; - el.offsetHeight; // reflow - el.style.height = 0; - }, - afterLeave(el) { - el.style.height = null; - }, - }, +const bodyId = ref(getUniqueId()); + +const showBody = ref( + props.persistKey && + localStorage.getItem(localStoragePrefix + props.persistKey) + ? localStorage.getItem(localStoragePrefix + props.persistKey) === "t" + : props.expanded, +); + +const animation = defaultStore.state.animation; + +watch(showBody, () => { + if (props.persistKey) { + localStorage.setItem( + localStoragePrefix + props.persistKey, + showBody.value ? "t" : "f", + ); + } +}); + +function toggleContent(show: boolean) { + showBody.value = show; +} + +function enter(el) { + const elementHeight = el.getBoundingClientRect().height; + el.style.height = 0; + el.offsetHeight; // reflow + // biome-ignore lint/style/useTemplate: <explanation> + el.style.height = elementHeight + "px"; +} +function afterEnter(el) { + el.style.height = null; +} +function leave(el) { + const elementHeight = el.getBoundingClientRect().height; + // biome-ignore lint/style/useTemplate: <explanation> + el.style.height = elementHeight + "px"; + el.offsetHeight; // reflow + el.style.height = 0; +} +function afterLeave(el) { + el.style.height = null; +} + +defineExpose({ + toggleContent, + enter, + afterEnter, + leave, + afterLeave, }); </script> From c4a093209f9b30a9137e0f0b60db3cdeb6c5a8d6 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Thu, 11 Apr 2024 18:00:37 +0800 Subject: [PATCH 023/110] fix type of MkEmojiPicker --- .../client/src/components/MkEmojiPicker.vue | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue index ac2e1a8111..4cb9bfad48 100644 --- a/packages/client/src/components/MkEmojiPicker.vue +++ b/packages/client/src/components/MkEmojiPicker.vue @@ -180,6 +180,11 @@ import { i18n } from "@/i18n"; import { defaultStore } from "@/store"; import icon from "@/scripts/icon"; +// FIXME: This variable doesn't seem to be used at all. I don't know why it was here. +const isActive = ref<boolean>(); + +type EmojiDef = string | entities.CustomEmoji | UnicodeEmojiDef; + const props = withDefaults( defineProps<{ showPinned?: boolean; @@ -193,7 +198,7 @@ const props = withDefaults( ); const emit = defineEmits<{ - (ev: "chosen", v: string, ev: MouseEvent): void; + chosen: [v: string, ev?: MouseEvent], }>(); const search = ref<HTMLInputElement>(); @@ -411,12 +416,18 @@ function reset() { } function getKey( - emoji: string | entities.CustomEmoji | UnicodeEmojiDef, + emoji: EmojiDef, ): string { - return typeof emoji === "string" ? emoji : emoji.emoji || `:${emoji.name}:`; + if (typeof emoji === "string") { + return emoji; + } + if ("emoji" in emoji) { + return emoji.emoji; + } + return `:${emoji.name}:`; } -function chosen(emoji: any, ev?: MouseEvent) { +function chosen(emoji: EmojiDef, ev?: MouseEvent) { const el = ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined); if (el) { @@ -432,22 +443,33 @@ function chosen(emoji: any, ev?: MouseEvent) { // 最近使った絵文字更新 if (!pinned.value.includes(key)) { let recents = defaultStore.state.recentlyUsedEmojis; - recents = recents.filter((emoji: any) => emoji !== key); + recents = recents.filter((emoji) => emoji !== key); recents.unshift(key); defaultStore.set("recentlyUsedEmojis", recents.splice(0, 32)); } } -function paste(event: ClipboardEvent) { - const paste = (event.clipboardData || window.clipboardData).getData("text"); - if (done(paste)) { +async function paste(event: ClipboardEvent) { + let pasteStr: string | null = null; + if (event.clipboardData) { + pasteStr = event.clipboardData.getData("text"); + } else { + // Use native api + try { + pasteStr = await window.navigator.clipboard.readText(); + } catch(_err) { + // Reading the clipboard requires permission, and the user did not give it + } + } + if (done(pasteStr)) { event.preventDefault(); } } -function done(query?: any): boolean | void { +function done(query?: string | null): boolean { + // biome-ignore lint/style/noParameterAssign: assign it intentially if (query == null) query = q.value; - if (query == null || typeof query !== "string") return; + if (query == null || typeof query !== "string") return false; const q2 = query.replaceAll(":", ""); const exactMatchCustom = customEmojis.find((emoji) => emoji.name === q2); @@ -470,6 +492,7 @@ function done(query?: any): boolean | void { chosen(searchResultUnicode.value[0]); return true; } + return false; } onMounted(() => { From ef94ba1474be829ee606ec4151624b3df71563d4 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 00:20:51 +0800 Subject: [PATCH 024/110] refactor: fix type errrors of some components --- packages/client/src/components/MkEmojiPicker.vue | 8 +++----- packages/client/src/components/MkEmojiPickerDialog.vue | 4 ++-- packages/client/src/components/MkFollowButton.vue | 2 +- packages/client/src/components/MkForgotPassword.vue | 6 +++--- packages/client/src/components/MkFormulaCore.vue | 4 ++-- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue index 4cb9bfad48..afc2f15817 100644 --- a/packages/client/src/components/MkEmojiPicker.vue +++ b/packages/client/src/components/MkEmojiPicker.vue @@ -198,7 +198,7 @@ const props = withDefaults( ); const emit = defineEmits<{ - chosen: [v: string, ev?: MouseEvent], + chosen: [v: string, ev?: MouseEvent]; }>(); const search = ref<HTMLInputElement>(); @@ -415,9 +415,7 @@ function reset() { q.value = ""; } -function getKey( - emoji: EmojiDef, -): string { +function getKey(emoji: EmojiDef): string { if (typeof emoji === "string") { return emoji; } @@ -457,7 +455,7 @@ async function paste(event: ClipboardEvent) { // Use native api try { pasteStr = await window.navigator.clipboard.readText(); - } catch(_err) { + } catch (_err) { // Reading the clipboard requires permission, and the user did not give it } } diff --git a/packages/client/src/components/MkEmojiPickerDialog.vue b/packages/client/src/components/MkEmojiPickerDialog.vue index 7c3319fb9f..426bbf9469 100644 --- a/packages/client/src/components/MkEmojiPickerDialog.vue +++ b/packages/client/src/components/MkEmojiPickerDialog.vue @@ -51,7 +51,7 @@ withDefaults( ); const emit = defineEmits<{ - (ev: "done", v: any): void; + (ev: "done", v: string): void; (ev: "close"): void; (ev: "closed"): void; }>(); @@ -64,7 +64,7 @@ function checkForShift(ev?: MouseEvent) { modal.value?.close(ev); } -function chosen(emoji: any, ev: MouseEvent) { +function chosen(emoji: string, ev?: MouseEvent) { emit("done", emoji); checkForShift(ev); } diff --git a/packages/client/src/components/MkFollowButton.vue b/packages/client/src/components/MkFollowButton.vue index ffe1de72af..0920001c7b 100644 --- a/packages/client/src/components/MkFollowButton.vue +++ b/packages/client/src/components/MkFollowButton.vue @@ -8,7 +8,7 @@ <i :class="icon('ph-dots-three-outline')"></i> </button> <button - v-if="!hideFollowButton && isSignedIn && me.id != user.id" + v-if="!hideFollowButton && isSignedIn && me!.id != user.id" v-tooltip="full ? null : `${state} ${user.name || user.username}`" class="kpoogebi _button follow-button" :class="{ diff --git a/packages/client/src/components/MkForgotPassword.vue b/packages/client/src/components/MkForgotPassword.vue index feaaa5f120..2ca1a87e87 100644 --- a/packages/client/src/components/MkForgotPassword.vue +++ b/packages/client/src/components/MkForgotPassword.vue @@ -3,7 +3,7 @@ ref="dialog" :width="370" :height="400" - @close="dialog.close()" + @close="dialog!.close()" @closed="emit('closed')" > <template #header>{{ i18n.ts.forgotPassword }}</template> @@ -76,7 +76,7 @@ const emit = defineEmits<{ (ev: "closed"): void; }>(); -const dialog: InstanceType<typeof XModalWindow> = ref(); +const dialog = ref<InstanceType<typeof XModalWindow> | null>(null); const username = ref(""); const email = ref(""); @@ -89,7 +89,7 @@ async function onSubmit() { email: email.value, }); emit("done"); - dialog.value.close(); + dialog.value!.close(); } </script> diff --git a/packages/client/src/components/MkFormulaCore.vue b/packages/client/src/components/MkFormulaCore.vue index 2db4c7d00d..d7f251dbcb 100644 --- a/packages/client/src/components/MkFormulaCore.vue +++ b/packages/client/src/components/MkFormulaCore.vue @@ -19,10 +19,10 @@ export default defineComponent({ }, }, computed: { - compiledFormula(): any { + compiledFormula() { const katexString = katex.renderToString(this.formula, { throwOnError: false, - } as any); + }); return this.block ? `<div style="text-align:center">${katexString}</div>` : katexString; From d8b4eb6f5e173be5f0ae1d17bab793e03d036062 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 00:22:49 +0800 Subject: [PATCH 025/110] fix: gallery posts not show & fix types --- .../src/models/repositories/gallery-post.ts | 4 ++- .../backend/src/models/schema/gallery-post.ts | 12 ++++++- .../src/components/MkGalleryPostPreview.vue | 20 +++++++++-- packages/client/src/pages/gallery/post.vue | 34 ++++++++++--------- packages/firefish-js/src/api.types.ts | 26 ++++++++++++-- packages/firefish-js/src/entities.ts | 16 ++++++++- packages/firefish-js/src/streaming.types.ts | 8 +++-- 7 files changed, 93 insertions(+), 27 deletions(-) diff --git a/packages/backend/src/models/repositories/gallery-post.ts b/packages/backend/src/models/repositories/gallery-post.ts index d91fb9de2a..c4078e9091 100644 --- a/packages/backend/src/models/repositories/gallery-post.ts +++ b/packages/backend/src/models/repositories/gallery-post.ts @@ -19,7 +19,9 @@ export const GalleryPostRepository = db.getRepository(GalleryPost).extend({ createdAt: post.createdAt.toISOString(), updatedAt: post.updatedAt.toISOString(), userId: post.userId, - user: Users.pack(post.user || post.userId, me), + user: Users.pack(post.user || post.userId, me, { + detail: true, + }), title: post.title, description: post.description, fileIds: post.fileIds, diff --git a/packages/backend/src/models/schema/gallery-post.ts b/packages/backend/src/models/schema/gallery-post.ts index 9ac348e1fb..ae22507643 100644 --- a/packages/backend/src/models/schema/gallery-post.ts +++ b/packages/backend/src/models/schema/gallery-post.ts @@ -38,7 +38,7 @@ export const packedGalleryPostSchema = { }, user: { type: "object", - ref: "UserLite", + ref: "UserDetailed", optional: false, nullable: false, }, @@ -79,5 +79,15 @@ export const packedGalleryPostSchema = { optional: false, nullable: false, }, + isLiked: { + type: "boolean", + optional: true, + nullable: false, + }, + likedCount: { + type: "number", + optional: false, + nullable: false, + }, }, } as const; diff --git a/packages/client/src/components/MkGalleryPostPreview.vue b/packages/client/src/components/MkGalleryPostPreview.vue index e393c6acc8..cbef1c3d75 100644 --- a/packages/client/src/components/MkGalleryPostPreview.vue +++ b/packages/client/src/components/MkGalleryPostPreview.vue @@ -2,10 +2,24 @@ <MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel"> <div class="thumbnail"> <ImgWithBlurhash + v-if="post.files && post.files.length > 0" class="img" :src="post.files[0].thumbnailUrl" :hash="post.files[0].blurhash" /> + <div + v-else + class="_fullinfo" + > + <!-- If there is no picture + This can happen if the user deletes the image in the drive + --> + <img + src="/static-assets/badges/not-found.webp" + class="img" + :alt="i18n.ts.notFound" + /> + </div> </div> <article> <header> @@ -20,9 +34,11 @@ <script lang="ts" setup> import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue"; +import { i18n } from "@/i18n"; +import type { entities } from "firefish-js"; -const props = defineProps<{ - post: any; +defineProps<{ + post: entities.GalleryPost; }>(); </script> diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue index 6f5e29ed0b..f8c92c1867 100644 --- a/packages/client/src/pages/gallery/post.vue +++ b/packages/client/src/pages/gallery/post.vue @@ -22,7 +22,7 @@ <div class="body _block"> <div class="title">{{ post.title }}</div> <div class="description"> - <Mfm :text="post.description" /> + <Mfm :text="post.description || ''" /> </div> <div class="info"> <i :class="icon('ph-clock')"></i> @@ -59,7 +59,7 @@ <div class="other"> <button v-if=" - isSignedIn && me.id === post.user.id + isSignedIn && me!.id === post.user.id " v-tooltip="i18n.ts.toEdit" v-click-anime @@ -105,7 +105,7 @@ <MkAcct :user="post.user" /> </div> <MkFollowButton - v-if="!me || me.id != post.user.id" + v-if="!isSignedIn || me!.id != post.user.id" :user="post.user" :inline="true" :transparent="false" @@ -140,7 +140,7 @@ </MkPagination> </MkContainer> </div> - <MkError v-else-if="error" @retry="fetch()" /> + <MkError v-else-if="error" @retry="fetchPost()" /> <MkLoading v-else /> </transition> </div> @@ -163,7 +163,8 @@ import { definePageMetadata } from "@/scripts/page-metadata"; import { shareAvailable } from "@/scripts/share-available"; import { defaultStore } from "@/store"; import icon from "@/scripts/icon"; -import { isSignedIn } from "@/me"; +import { isSignedIn, me } from "@/me"; +import type { entities } from "firefish-js"; const router = useRouter(); @@ -171,18 +172,19 @@ const props = defineProps<{ postId: string; }>(); -const post = ref(null); +const post = ref<entities.GalleryPost | null>(null); const error = ref(null); const otherPostsPagination = { endpoint: "users/gallery/posts" as const, limit: 6, params: computed(() => ({ - userId: post.value.user.id, + userId: post.value!.user.id, })), }; function fetchPost() { post.value = null; + error.value = null; os.api("gallery/posts/show", { postId: props.postId, }) @@ -196,15 +198,15 @@ function fetchPost() { function share() { navigator.share({ - title: post.value.title, - text: post.value.description, - url: `${url}/gallery/${post.value.id}`, + title: post.value!.title, + text: post.value!.description || undefined, + url: `${url}/gallery/${post.value!.id}`, }); } function shareWithNote() { os.post({ - initialText: `${post.value.title} ${url}/gallery/${post.value.id}`, + initialText: `${post.value!.title} ${url}/gallery/${post.value!.id}`, }); } @@ -212,8 +214,8 @@ function like() { os.api("gallery/posts/like", { postId: props.postId, }).then(() => { - post.value.isLiked = true; - post.value.likedCount++; + post.value!.isLiked = true; + post.value!.likedCount++; }); } @@ -221,13 +223,13 @@ async function unlike() { os.api("gallery/posts/unlike", { postId: props.postId, }).then(() => { - post.value.isLiked = false; - post.value.likedCount--; + post.value!.isLiked = false; + post.value!.likedCount--; }); } function edit() { - router.push(`/gallery/${post.value.id}/edit`); + router.push(`/gallery/${post.value!.id}/edit`); } watch(() => props.postId, fetchPost, { immediate: true }); diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts index 9d1123e7ad..3571d79546 100644 --- a/packages/firefish-js/src/api.types.ts +++ b/packages/firefish-js/src/api.types.ts @@ -432,7 +432,12 @@ export type Endpoints = { "gallery/posts/create": { req: TODO; res: TODO }; "gallery/posts/delete": { req: { postId: GalleryPost["id"] }; res: null }; "gallery/posts/like": { req: TODO; res: TODO }; - "gallery/posts/show": { req: TODO; res: TODO }; + "gallery/posts/show": { + req: { + postId: GalleryPost["id"]; + }; + res: GalleryPost; + }; "gallery/posts/unlike": { req: TODO; res: TODO }; "gallery/posts/update": { req: TODO; res: TODO }; @@ -474,7 +479,14 @@ export type Endpoints = { res: NoteFavorite[]; }; "i/gallery/likes": { req: TODO; res: TODO }; - "i/gallery/posts": { req: TODO; res: TODO }; + "i/gallery/posts": { + req: { + limit?: number; + sinceId?: NoteFavorite["id"]; + untilId?: NoteFavorite["id"]; + }; + res: GalleryPost[]; + }; "i/get-word-muted-notes-count": { req: TODO; res: TODO }; "i/import-following": { req: TODO; res: TODO }; "i/import-user-lists": { req: TODO; res: TODO }; @@ -890,7 +902,15 @@ export type Endpoints = { }; res: FollowingFolloweePopulated[]; }; - "users/gallery/posts": { req: TODO; res: TODO }; + "users/gallery/posts": { + req: { + userId: User["id"]; + limit?: number; + sinceId?: NoteFavorite["id"]; + untilId?: NoteFavorite["id"]; + }; + res: GalleryPost[]; + }; "users/get-frequently-replied-users": { req: TODO; res: TODO }; "users/groups/create": { req: TODO; res: TODO }; "users/groups/delete": { req: { groupId: UserGroup["id"] }; res: null }; diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index cafbdc7864..35dff298db 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -136,7 +136,21 @@ export type DriveFile = { export type DriveFolder = TODO; -export type GalleryPost = TODO; +export type GalleryPost = { + id: ID; + createdAt: DateString; + updatedAt: DateString; + title: string; + description: string | null; + userId: User["id"]; + user: UserDetailed; + fileIds?: DriveFile["id"][]; + files?: DriveFile[]; + tags?: string[]; + isSensitive: boolean; + isLiked?: boolean; + likedCount: number; +}; export type Note = { id: ID; diff --git a/packages/firefish-js/src/streaming.types.ts b/packages/firefish-js/src/streaming.types.ts index 18c7682608..5b81780271 100644 --- a/packages/firefish-js/src/streaming.types.ts +++ b/packages/firefish-js/src/streaming.types.ts @@ -8,7 +8,9 @@ import type { Notification, PageEvent, User, + UserDetailed, UserGroup, + UserLite, } from "./entities"; import type { Connection } from "./streaming"; @@ -26,9 +28,9 @@ export type Channels = { mention: (payload: Note) => void; reply: (payload: Note) => void; renote: (payload: Note) => void; - follow: (payload: User) => void; // 自分が他人をフォローしたとき - followed: (payload: User) => void; // 他人が自分をフォローしたとき - unfollow: (payload: User) => void; // 自分が他人をフォロー解除したとき + follow: (payload: UserDetailed) => void; // 自分が他人をフォローしたとき + followed: (payload: UserLite) => void; // 他人が自分をフォローしたとき + unfollow: (payload: UserDetailed) => void; // 自分が他人をフォロー解除したとき meUpdated: (payload: MeDetailed) => void; pageEvent: (payload: PageEvent) => void; urlUploadFinished: (payload: { marker: string; file: DriveFile }) => void; From e6b7eca77501cf41959e567a7a95955b3ced29dc Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 01:12:18 +0800 Subject: [PATCH 026/110] fix type errors of components --- .../client/src/components/MkImageViewer.vue | 14 ++-- .../src/components/MkImgWithBlurhash.vue | 8 +- .../src/components/MkInstanceCardMini.vue | 7 +- .../src/components/MkInstanceSelectDialog.vue | 8 +- .../client/src/components/MkInstanceStats.vue | 81 ++++++++++--------- .../client/src/components/MkLaunchPad.vue | 4 +- .../client/src/components/global/MkEmoji.vue | 4 +- .../client/src/scripts/use-chart-tooltip.ts | 15 +++- packages/firefish-js/src/api.types.ts | 11 +++ packages/firefish-js/src/entities.ts | 2 + 10 files changed, 88 insertions(+), 66 deletions(-) diff --git a/packages/client/src/components/MkImageViewer.vue b/packages/client/src/components/MkImageViewer.vue index c65136718d..27c4d421aa 100644 --- a/packages/client/src/components/MkImageViewer.vue +++ b/packages/client/src/components/MkImageViewer.vue @@ -2,16 +2,16 @@ <MkModal ref="modal" :z-priority="'middle'" - @click="modal.close()" + @click="modal!.close()" @closed="emit('closed')" > <div class="xubzgfga"> <header>{{ image.name }}</header> <img :src="image.url" - :alt="image.comment" - :title="image.comment" - @click="modal.close()" + :alt="image.comment || undefined" + :title="image.comment || undefined" + @click="modal!.close()" /> <footer> <span>{{ image.type }}</span> @@ -33,7 +33,7 @@ import bytes from "@/filters/bytes"; import number from "@/filters/number"; import MkModal from "@/components/MkModal.vue"; -const props = withDefaults( +withDefaults( defineProps<{ image: entities.DriveFile; }>(), @@ -41,10 +41,10 @@ const props = withDefaults( ); const emit = defineEmits<{ - (ev: "closed"): void; + closed: []; }>(); -const modal = ref<InstanceType<typeof MkModal>>(); +const modal = ref<InstanceType<typeof MkModal> | null>(null); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/MkImgWithBlurhash.vue b/packages/client/src/components/MkImgWithBlurhash.vue index 5e70515a93..31aa0e1842 100644 --- a/packages/client/src/components/MkImgWithBlurhash.vue +++ b/packages/client/src/components/MkImgWithBlurhash.vue @@ -4,20 +4,20 @@ ref="canvas" :width="size" :height="size" - :title="title" + :title="title || undefined" /> <img v-if="src" :src="src" - :title="title" + :title="title || undefined" :type="type" - :alt="alt" + :alt="alt || undefined" :class="{ cover, wide: largestDimension === 'width', tall: largestDimension === 'height', }" - :style="{ 'object-fit': cover ? 'cover' : null }" + :style="{ 'object-fit': cover ? 'cover' : undefined }" loading="lazy" @load="onLoad" /> diff --git a/packages/client/src/components/MkInstanceCardMini.vue b/packages/client/src/components/MkInstanceCardMini.vue index c44cfa291f..7c722f07f2 100644 --- a/packages/client/src/components/MkInstanceCardMini.vue +++ b/packages/client/src/components/MkInstanceCardMini.vue @@ -23,17 +23,14 @@ </template> <script lang="ts" setup> -import { ref } from "vue"; - import type { entities } from "firefish-js"; -import * as os from "@/os"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; -const props = defineProps<{ +defineProps<{ instance: entities.Instance; }>(); -function getInstanceIcon(instance): string { +function getInstanceIcon(instance: entities.Instance): string { return ( getProxiedImageUrlNullable(instance.faviconUrl, "preview") ?? getProxiedImageUrlNullable(instance.iconUrl, "preview") ?? diff --git a/packages/client/src/components/MkInstanceSelectDialog.vue b/packages/client/src/components/MkInstanceSelectDialog.vue index 92c060e4dd..e1e0da550c 100644 --- a/packages/client/src/components/MkInstanceSelectDialog.vue +++ b/packages/client/src/components/MkInstanceSelectDialog.vue @@ -65,14 +65,14 @@ import * as os from "@/os"; import { i18n } from "@/i18n"; const emit = defineEmits<{ - (ev: "ok", selected: entities.Instance): void; - (ev: "cancel"): void; - (ev: "closed"): void; + ok: [selected: entities.Instance]; + cancel: []; + closed: []; }>(); const hostname = ref(""); const instances = ref<entities.Instance[]>([]); -const selected = ref<entities.Instance | null>(); +const selected = ref<entities.Instance | null>(null); const dialogEl = ref<InstanceType<typeof XModalWindow>>(); let searchOrderLatch = 0; diff --git a/packages/client/src/components/MkInstanceStats.vue b/packages/client/src/components/MkInstanceStats.vue index 7ae2278d9f..72f987ec84 100644 --- a/packages/client/src/components/MkInstanceStats.vue +++ b/packages/client/src/components/MkInstanceStats.vue @@ -52,6 +52,7 @@ import { i18n } from "@/i18n"; import MkActiveUsersHeatmap from "@/components/MkActiveUsersHeatmap.vue"; import MkFolder from "@/components/MkFolder.vue"; import { initChart } from "@/scripts/init-chart"; +import type { entities } from "firefish-js"; initChart(); @@ -67,7 +68,18 @@ const { handler: externalTooltipHandler2 } = useChartTooltip({ position: "middle", }); -function createDoughnut(chartEl, tooltip, data) { +interface ColorData { + name: string; + color: string | undefined; + value: number; + onClick?: () => void; +} + +function createDoughnut( + chartEl: HTMLCanvasElement, + tooltip: typeof externalTooltipHandler1, + data: ColorData[], +) { const chartInstance = new Chart(chartEl, { type: "doughnut", data: { @@ -96,13 +108,13 @@ function createDoughnut(chartEl, tooltip, data) { }, onClick: (ev) => { const hit = chartInstance.getElementsAtEventForMode( - ev, + ev as unknown as Event, "nearest", { intersect: true }, false, )[0]; - if (hit && data[hit.index].onClick) { - data[hit.index].onClick(); + if (hit) { + data[hit.index].onClick?.(); } }, plugins: { @@ -124,48 +136,41 @@ function createDoughnut(chartEl, tooltip, data) { return chartInstance; } +function instance2ColorData(x: entities.Instance): ColorData { + return { + name: x.host, + color: x.themeColor || undefined, + value: x.followersCount, + onClick: () => { + os.pageWindow(`/instance-info/${x.host}`); + }, + }; +} + onMounted(() => { os.apiGet("federation/stats", { limit: 30 }).then((fedStats) => { createDoughnut( - subDoughnutEl.value, + subDoughnutEl.value!, externalTooltipHandler1, - fedStats.topSubInstances - .map((x) => ({ - name: x.host, - color: x.themeColor, - value: x.followersCount, - onClick: () => { - os.pageWindow(`/instance-info/${x.host}`); - }, - })) - .concat([ - { - name: "(other)", - color: "#80808080", - value: fedStats.otherFollowersCount, - }, - ]), + fedStats.topSubInstances.map(instance2ColorData).concat([ + { + name: "(other)", + color: "#80808080", + value: fedStats.otherFollowersCount, + }, + ]), ); createDoughnut( - pubDoughnutEl.value, + pubDoughnutEl.value!, externalTooltipHandler2, - fedStats.topPubInstances - .map((x) => ({ - name: x.host, - color: x.themeColor, - value: x.followingCount, - onClick: () => { - os.pageWindow(`/instance-info/${x.host}`); - }, - })) - .concat([ - { - name: "(other)", - color: "#80808080", - value: fedStats.otherFollowingCount, - }, - ]), + fedStats.topPubInstances.map(instance2ColorData).concat([ + { + name: "(other)", + color: "#80808080", + value: fedStats.otherFollowingCount, + }, + ]), ); }); }); diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue index ddb4dc69d2..d17857dc43 100644 --- a/packages/client/src/components/MkLaunchPad.vue +++ b/packages/client/src/components/MkLaunchPad.vue @@ -6,7 +6,7 @@ :anchor="anchor" :transparent-bg="true" :src="src" - @click="modal.close()" + @click="modal!.close()" @closed="emit('closed')" > <div @@ -109,7 +109,7 @@ const items = Object.keys(navbarItemDef) })); function close() { - modal.value.close(); + modal.value!.close(); } </script> diff --git a/packages/client/src/components/global/MkEmoji.vue b/packages/client/src/components/global/MkEmoji.vue index 86e3d344b6..57380ba6f4 100644 --- a/packages/client/src/components/global/MkEmoji.vue +++ b/packages/client/src/components/global/MkEmoji.vue @@ -54,8 +54,8 @@ const url = computed(() => { return char2filePath(char.value); } else { return defaultStore.state.disableShowingAnimatedImages - ? getStaticImageUrl(customEmoji.value.url) - : customEmoji.value.url; + ? getStaticImageUrl(customEmoji.value!.url) + : customEmoji.value!.url; } }); const alt = computed(() => diff --git a/packages/client/src/scripts/use-chart-tooltip.ts b/packages/client/src/scripts/use-chart-tooltip.ts index 573b5b4c7c..515c60a179 100644 --- a/packages/client/src/scripts/use-chart-tooltip.ts +++ b/packages/client/src/scripts/use-chart-tooltip.ts @@ -1,6 +1,13 @@ import { onDeactivated, onUnmounted, ref } from "vue"; import * as os from "@/os"; import MkChartTooltip from "@/components/MkChartTooltip.vue"; +import type { Color, TooltipOptions } from "chart.js"; + +type ToolTipSerie = { + backgroundColor: Color; + borderColor: Color; + text: string; +}; export function useChartTooltip( opts: { position: "top" | "middle" } = { position: "top" }, @@ -8,9 +15,9 @@ export function useChartTooltip( const tooltipShowing = ref(false); const tooltipX = ref(0); const tooltipY = ref(0); - const tooltipTitle = ref(null); - const tooltipSeries = ref(null); - let disposeTooltipComponent; + const tooltipTitle = ref<string | null>(null); + const tooltipSeries = ref<ToolTipSerie[] | null>(null); + let disposeTooltipComponent: () => void; os.popup( MkChartTooltip, @@ -34,7 +41,7 @@ export function useChartTooltip( tooltipShowing.value = false; }); - function handler(context) { + const handler: TooltipOptions["external"] = (context) => { if (context.tooltip.opacity === 0) { tooltipShowing.value = false; return; diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts index 3571d79546..efa97d0a96 100644 --- a/packages/firefish-js/src/api.types.ts +++ b/packages/firefish-js/src/api.types.ts @@ -405,6 +405,17 @@ export type Endpoints = { res: Instance[]; }; "federation/show-instance": { req: { host: string }; res: Instance }; + "federation/stats": { + req: { + limit?: number; + }; + res: { + topSubInstances: Instance[]; + otherFollowersCount: number; + topPubInstances: Instance[]; + otherFollowingCount: number; + }; + }; "federation/update-remote-user": { req: { userId: User["id"] }; res: null }; "federation/users": { req: { diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index 35dff298db..7bf1183c2d 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -539,6 +539,8 @@ export type Instance = { lastCommunicatedAt: DateString; isNotResponding: boolean; isSuspended: boolean; + isBlocked: boolean; + isSilenced: boolean; softwareName: string | null; softwareVersion: string | null; openRegistrations: boolean | null; From 38668c4c115cf0b406012de240a693f944c6c64d Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 01:32:04 +0800 Subject: [PATCH 027/110] fix type errors --- .../src/components/MkInstanceTicker.vue | 6 ++-- .../src/components/MkManyAnnouncements.vue | 2 +- packages/client/src/components/MkMedia.vue | 2 +- .../client/src/components/MkMediaBanner.vue | 2 +- .../client/src/scripts/use-chart-tooltip.ts | 2 +- packages/firefish-js/src/entities.ts | 35 ++++++++++++++++++- 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/client/src/components/MkInstanceTicker.vue b/packages/client/src/components/MkInstanceTicker.vue index 46da872c2c..498303ef8e 100644 --- a/packages/client/src/components/MkInstanceTicker.vue +++ b/packages/client/src/components/MkInstanceTicker.vue @@ -35,12 +35,12 @@ const ticker = ref<HTMLElement | null>(null); // if no instance data is given, this is for the local instance const instance = props.instance ?? { - faviconUrl: Instance.faviconUrl || Instance.iconUrl || "/favicon.ico", + faviconUrl: Instance.iconUrl || "/favicon.ico", name: instanceName, themeColor: ( document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement )?.content, - softwareName: Instance.softwareName ?? "Firefish", + softwareName: "Firefish", softwareVersion: version, }; @@ -67,7 +67,7 @@ const commonNames = new Map<string, string>([ ["wxwclub", "wxwClub"], ]); -const capitalize = (s: string) => { +const capitalize = (s?: string | null) => { if (s == null) return "Unknown"; if (commonNames.has(s)) return commonNames.get(s); return s[0].toUpperCase() + s.slice(1); diff --git a/packages/client/src/components/MkManyAnnouncements.vue b/packages/client/src/components/MkManyAnnouncements.vue index c65cd0a9ab..903891b64c 100644 --- a/packages/client/src/components/MkManyAnnouncements.vue +++ b/packages/client/src/components/MkManyAnnouncements.vue @@ -23,7 +23,7 @@ import { i18n } from "@/i18n"; const modal = shallowRef<InstanceType<typeof MkModal>>(); const checkAnnouncements = () => { - modal.value.close(); + modal.value!.close(); location.href = "/announcements"; }; </script> diff --git a/packages/client/src/components/MkMedia.vue b/packages/client/src/components/MkMedia.vue index ef8912f138..c05a11e448 100644 --- a/packages/client/src/components/MkMedia.vue +++ b/packages/client/src/components/MkMedia.vue @@ -50,7 +50,7 @@ > <video :poster="media.thumbnailUrl" - :aria-label="media.comment" + :aria-label="media.comment || undefined" preload="none" controls playsinline diff --git a/packages/client/src/components/MkMediaBanner.vue b/packages/client/src/components/MkMediaBanner.vue index d703fde470..cee8425c36 100644 --- a/packages/client/src/components/MkMediaBanner.vue +++ b/packages/client/src/components/MkMediaBanner.vue @@ -64,7 +64,7 @@ import "vue-plyr/dist/vue-plyr.css"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; -const props = withDefaults( +withDefaults( defineProps<{ media: entities.DriveFile; }>(), diff --git a/packages/client/src/scripts/use-chart-tooltip.ts b/packages/client/src/scripts/use-chart-tooltip.ts index 515c60a179..bc6c5b1eeb 100644 --- a/packages/client/src/scripts/use-chart-tooltip.ts +++ b/packages/client/src/scripts/use-chart-tooltip.ts @@ -63,7 +63,7 @@ export function useChartTooltip( } else if (opts.position === "middle") { tooltipY.value = rect.top + window.scrollY + context.tooltip.caretY; } - } + }; return { handler, diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index 7bf1183c2d..34c958ea5f 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -371,7 +371,40 @@ export type LiteInstanceMetadata = { }; export type DetailedInstanceMetadata = LiteInstanceMetadata & { - features: Record<string, any>; + features: { + registration: boolean; + localTimeLine: boolean; + recommendedTimeLine: boolean; + globalTimeLine: boolean; + searchFilters: boolean; + hcaptcha: boolean; + recaptcha: boolean; + objectStorage: boolean; + serviceWorker: boolean; + miauth?: boolean; + }; + langs: string[]; + moreUrls: object; + repositoryUrl: string; + feedbackUrl: string; + defaultDarkTheme: string | null; + defaultLightTheme: string | null; + enableGuestTimeline: boolean; + cacheRemoteFiles: boolean; + emailRequiredForSignup: boolean; + mascotImageUrl: string; + bannerUrl: string; + errorImageUrl: string; + iconUrl: string | null; + maxCaptionTextLength: number; + requireSetup: boolean; + translatorAvailable: boolean; + proxyAccountName: string | null; + secureMode?: boolean; + privateMode?: boolean; + defaultReaction: string; + donationLink?: string | null; + enableServerMachineStats?: boolean; }; export type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata; From 9c75dd0b263b5458dbe6234212a20ec0948e5a9c Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 09:33:27 +0800 Subject: [PATCH 028/110] refactor: define more cleared type of os.ts --- packages/client/src/components/MkLink.vue | 2 +- packages/client/src/os.ts | 28 ++++++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/MkLink.vue b/packages/client/src/components/MkLink.vue index 4add7c32da..34db74397e 100644 --- a/packages/client/src/components/MkLink.vue +++ b/packages/client/src/components/MkLink.vue @@ -42,7 +42,7 @@ useTooltip(el, (showing) => { os.popup( defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")), { - showing, + showing: showing.value, url: props.url, source: el.value, }, diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 328b961b21..646f627d54 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -3,7 +3,13 @@ import { EventEmitter } from "eventemitter3"; import { type entities, api as firefishApi } from "firefish-js"; import insertTextAtCursor from "insert-text-at-cursor"; -import type { Component, Ref } from "vue"; +import type { + Component, + ComponentPublicInstance, + DefineComponent, + EmitsOptions, + Ref, +} from "vue"; import { defineAsyncComponent, markRaw, ref } from "vue"; import { i18n } from "./i18n"; import MkDialog from "@/components/MkDialog.vue"; @@ -196,13 +202,23 @@ export function claimZIndex( let uniqueId = 0; export function getUniqueId(): string { - return uniqueId++ + ""; + return String(uniqueId++); } -export async function popup( - component: Component, - props: Record<string, any>, - events = {}, +interface VueComponentConstructor<P, E> { + __isFragment?: never; + __isTeleport?: never; + __isSuspense?: never; + new (): { + $props: P; + }; + emits?: E; +} + +export async function popup<Props, Emits>( + component: VueComponentConstructor<Props, Emits>, + props: Props & Record<string, unknown>, + events: Partial<NonNullable<Emits>> | Record<string, never> = {}, disposeEvent?: string, ) { markRaw(component); From 95b91c639637f686999a1ae20d405ca15518b5d3 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 09:40:34 +0800 Subject: [PATCH 029/110] chore: remove unused type imports --- packages/client/src/os.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 646f627d54..426fd29084 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -5,9 +5,6 @@ import { type entities, api as firefishApi } from "firefish-js"; import insertTextAtCursor from "insert-text-at-cursor"; import type { Component, - ComponentPublicInstance, - DefineComponent, - EmitsOptions, Ref, } from "vue"; import { defineAsyncComponent, markRaw, ref } from "vue"; @@ -180,13 +177,11 @@ export function promiseDialog<T>( } let popupIdCount = 0; -export const popups = ref([]) as Ref< - { - id: any; - component: any; - props: Record<string, any>; - }[] ->; +export const popups = ref<{ + id: number; + component: Component; + props: Record<string, unknown>; +}[]>([]); const zIndexes = { low: 1000000, From f3b189a70c3f62faf111b7b8c62eb845642bf0bc Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 09:46:04 +0800 Subject: [PATCH 030/110] refactor: rewrite MkMediaCaption --- .../client/src/components/MkMediaCaption.vue | 190 ++++++++---------- packages/firefish-js/src/api.types.ts | 6 + 2 files changed, 90 insertions(+), 106 deletions(-) diff --git a/packages/client/src/components/MkMediaCaption.vue b/packages/client/src/components/MkMediaCaption.vue index 88bf24631b..815b7c5adf 100644 --- a/packages/client/src/components/MkMediaCaption.vue +++ b/packages/client/src/components/MkMediaCaption.vue @@ -1,5 +1,5 @@ <template> - <MkModal ref="modal" @click="done(true)" @closed="$emit('closed')"> + <MkModal ref="modal" @click="done(true)" @closed="emit('closed')"> <div class="container"> <div class="fullwidth top-caption"> <div class="mk-dialog"> @@ -48,9 +48,9 @@ <img id="imgtocaption" :src="image.url" - :alt="image.comment" - :title="image.comment" - @click="$refs.modal.close()" + :alt="image.comment || undefined" + :title="image.comment || undefined" + @click="modal!.close()" /> <footer> <span>{{ image.type }}</span> @@ -65,8 +65,8 @@ </MkModal> </template> -<script lang="ts"> -import { defineComponent } from "vue"; +<script lang="ts" setup> +import { computed, onBeforeUnmount, onMounted, ref } from "vue"; import insertTextAtCursor from "insert-text-at-cursor"; import { length } from "stringz"; import * as os from "@/os"; @@ -76,122 +76,100 @@ import bytes from "@/filters/bytes"; import number from "@/filters/number"; import { i18n } from "@/i18n"; import { instance } from "@/instance"; +import type { entities } from "firefish-js"; -export default defineComponent({ - components: { - MkModal, - MkButton, - }, - - props: { - image: { - type: Object, - required: true, - }, - title: { - type: String, - required: false, - }, +const props = withDefaults( + defineProps<{ + image: entities.DriveFile; input: { - required: true, - }, - showOkButton: { - type: Boolean, - default: true, - }, - showCaptionButton: { - type: Boolean, - default: true, - }, - showCancelButton: { - type: Boolean, - default: true, - }, - cancelableByBgClick: { - type: Boolean, - default: true, - }, - }, - - emits: ["done", "closed"], - - data() { - return { - inputValue: this.input.default ? this.input.default : null, - i18n, + placeholder: string; + default: string; }; + title?: string; + showOkButton?: boolean; + showCaptionButton?: boolean; + showCancelButton?: boolean; + cancelableByBgClick?: boolean; + }>(), + { + showOkButton: true, + showCaptionButton: true, + showCancelButton: true, + cancelableByBgClick: true, }, +); - computed: { - remainingLength(): number { - const maxCaptionLength = instance.maxCaptionTextLength ?? 512; - if (typeof this.inputValue !== "string") return maxCaptionLength; - return maxCaptionLength - length(this.inputValue); - }, - }, +const emit = defineEmits<{ + done: [v: { canceled: boolean; result?: string | null }]; + closed: []; +}>(); - mounted() { - document.addEventListener("keydown", this.onKeydown); - }, +const modal = ref<InstanceType<typeof MkModal> | null>(null); - beforeUnmount() { - document.removeEventListener("keydown", this.onKeydown); - }, +const inputValue = ref(props.input.default ? props.input.default : null); - methods: { - bytes, - number, +const remainingLength = computed(() => { + const maxCaptionLength = instance.maxCaptionTextLength ?? 512; + if (typeof inputValue.value !== "string") return maxCaptionLength; + return maxCaptionLength - length(inputValue.value); +}); - done(canceled, result?) { - this.$emit("done", { canceled, result }); - this.$refs.modal.close(); - }, +function done(canceled: boolean, result?: string | null) { + emit("done", { canceled, result }); + modal.value!.close(); +} - async ok() { - if (!this.showOkButton) return; +async function ok() { + if (!props.showOkButton) return; - const result = this.inputValue; - this.done(false, result); - }, + const result = inputValue.value; + done(false, result); +} - cancel() { - this.done(true); - }, +function cancel() { + done(true); +} - onBgClick() { - if (this.cancelableByBgClick) { - this.cancel(); - } - }, +// function onBgClick() { +// if (props.cancelableByBgClick) { +// cancel(); +// } +// } - onKeydown(evt) { - if (evt.which === 27) { - // ESC - this.cancel(); - } - }, +function onKeydown(evt) { + if (evt.which === 27) { + // ESC + cancel(); + } +} - onInputKeydown(evt) { - if (evt.which === 13) { - // Enter - if (evt.ctrlKey) { - evt.preventDefault(); - evt.stopPropagation(); - this.ok(); - } - } - }, +function onInputKeydown(evt) { + if (evt.which === 13) { + // Enter + if (evt.ctrlKey) { + evt.preventDefault(); + evt.stopPropagation(); + ok(); + } + } +} - caption() { - const img = document.getElementById("imgtocaption") as HTMLImageElement; - const ta = document.getElementById("captioninput") as HTMLTextAreaElement; - os.api("drive/files/caption-image", { - url: img.src, - }).then((text) => { - insertTextAtCursor(ta, text.slice(0, 512 - ta.value.length)); - }); - }, - }, +function caption() { + const img = document.getElementById("imgtocaption") as HTMLImageElement; + const ta = document.getElementById("captioninput") as HTMLTextAreaElement; + os.api("drive/files/caption-image", { + url: img.src, + }).then((text) => { + insertTextAtCursor(ta, text.slice(0, 512 - ta.value.length)); + }); +} + +onMounted(() => { + document.addEventListener("keydown", onKeydown); +}); + +onBeforeUnmount(() => { + document.removeEventListener("keydown", onKeydown); }); </script> diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts index efa97d0a96..73e9fca69f 100644 --- a/packages/firefish-js/src/api.types.ts +++ b/packages/firefish-js/src/api.types.ts @@ -279,6 +279,12 @@ export type Endpoints = { res: DriveFile[]; }; "drive/files/attached-notes": { req: TODO; res: Note[] }; + "drive/files/caption-image": { + req: { + url: string, + } + res: string, + }; "drive/files/check-existence": { req: TODO; res: TODO }; "drive/files/create": { req: TODO; res: TODO }; "drive/files/delete": { req: { fileId: DriveFile["id"] }; res: null }; From 5da03666b2fd29193c45ec88cf4f3766987a53bc Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 11:16:26 +0800 Subject: [PATCH 031/110] fix types of component --- biome.json | 3 +- .../client/src/components/MkDrive.file.vue | 7 ++-- .../client/src/components/MkLaunchPad.vue | 5 ++- .../client/src/components/MkMediaCaption.vue | 2 +- .../client/src/components/MkMediaList.vue | 32 +++++++++++-------- packages/client/src/components/MkMention.vue | 3 +- .../client/src/components/MkMenu.child.vue | 6 ++-- packages/client/src/components/MkMenu.vue | 27 ++++++++++------ .../client/src/components/MkMiniChart.vue | 2 +- packages/client/src/components/MkModal.vue | 20 ++++++++---- .../client/src/components/MkPopupMenu.vue | 2 +- packages/client/src/os.ts | 4 +-- 12 files changed, 68 insertions(+), 45 deletions(-) diff --git a/biome.json b/biome.json index 9bf08ad553..80f0d63eb4 100644 --- a/biome.json +++ b/biome.json @@ -21,7 +21,8 @@ "useImportType": "warn", "useShorthandFunctionType": "warn", "useTemplate": "warn", - "noNonNullAssertion": "off" + "noNonNullAssertion": "off", + "useNodejsImportProtocol": "off" } } } diff --git a/packages/client/src/components/MkDrive.file.vue b/packages/client/src/components/MkDrive.file.vue index 9d09da663e..80a428adc3 100644 --- a/packages/client/src/components/MkDrive.file.vue +++ b/packages/client/src/components/MkDrive.file.vue @@ -181,12 +181,15 @@ function describe() { image: props.file, }, { - done: (result) => { + done: (result: { + canceled: boolean, + result?: string | null, + }) => { if (!result || result.canceled) return; const comment = result.result; os.api("drive/files/update", { fileId: props.file.id, - comment: comment.length === 0 ? null : comment, + comment: comment || null, }); }, }, diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue index d17857dc43..084283c270 100644 --- a/packages/client/src/components/MkLaunchPad.vue +++ b/packages/client/src/components/MkLaunchPad.vue @@ -73,7 +73,10 @@ import { deviceKind } from "@/scripts/device-kind"; const props = withDefaults( defineProps<{ src?: HTMLElement; - anchor?: { x: string; y: string }; + anchor?: { + x: "left" | "center" | "right"; + y: "top" | "center" | "bottom"; + }; }>(), { anchor: () => ({ x: "right", y: "center" }), diff --git a/packages/client/src/components/MkMediaCaption.vue b/packages/client/src/components/MkMediaCaption.vue index 815b7c5adf..32ecceda21 100644 --- a/packages/client/src/components/MkMediaCaption.vue +++ b/packages/client/src/components/MkMediaCaption.vue @@ -100,7 +100,7 @@ const props = withDefaults( ); const emit = defineEmits<{ - done: [v: { canceled: boolean; result?: string | null }]; + done: [result: { canceled: boolean; result?: string | null }]; closed: []; }>(); diff --git a/packages/client/src/components/MkMediaList.vue b/packages/client/src/components/MkMediaList.vue index af93e24165..39f618779f 100644 --- a/packages/client/src/components/MkMediaList.vue +++ b/packages/client/src/components/MkMediaList.vue @@ -22,7 +22,7 @@ media.type.startsWith('video') || media.type.startsWith('image') " - :key="media.id" + :key="`m-${media.id}`" :class="{ image: media.type.startsWith('image') }" :data-id="media.id" :media="media" @@ -30,7 +30,7 @@ /> <XModPlayer v-else-if="isModule(media)" - :key="media.id" + :key="`p-${media.id}`" :module="media" /> </template> @@ -48,7 +48,7 @@ import "photoswipe/style.css"; import XBanner from "@/components/MkMediaBanner.vue"; import XMedia from "@/components/MkMedia.vue"; import XModPlayer from "@/components/MkModPlayer.vue"; -import * as os from "@/os"; +// import * as os from "@/os"; import { FILE_EXT_TRACKER_MODULES, FILE_TYPE_BROWSERSAFE, @@ -61,8 +61,8 @@ const props = defineProps<{ inDm?: boolean; }>(); -const gallery = ref(null); -const pswpZIndex = os.claimZIndex("middle"); +const gallery = ref<HTMLElement | null>(null); +// const pswpZIndex = os.claimZIndex("middle"); onMounted(() => { const lightbox = new PhotoSwipeLightbox({ @@ -79,7 +79,7 @@ onMounted(() => { src: media.url, w: media.properties.width, h: media.properties.height, - alt: media.comment, + alt: media.comment || undefined, }; if ( media.properties.orientation != null && @@ -89,7 +89,7 @@ onMounted(() => { } return item; }), - gallery: gallery.value, + gallery: gallery.value || undefined, children: ".image", thumbSelector: ".image img", loop: false, @@ -119,9 +119,13 @@ onMounted(() => { // element is children const { element } = itemData; + if (element == null) return; + const id = element.dataset.id; const file = props.mediaList.find((media) => media.id === id); + if (file == null) return; + itemData.src = file.url; itemData.w = Number(file.properties.width); itemData.h = Number(file.properties.height); @@ -132,12 +136,12 @@ onMounted(() => { [itemData.w, itemData.h] = [itemData.h, itemData.w]; } itemData.msrc = file.thumbnailUrl; - itemData.alt = file.comment; + itemData.alt = file.comment || undefined; itemData.thumbCropped = true; }); lightbox.on("uiRegister", () => { - lightbox.pswp.ui.registerElement({ + lightbox.pswp?.ui?.registerElement({ name: "altText", className: "pwsp__alt-text-container", appendTo: "wrapper", @@ -146,7 +150,7 @@ onMounted(() => { textBox.className = "pwsp__alt-text"; el.appendChild(textBox); - const preventProp = function (ev: Event): void { + const preventProp = (ev: Event): void => { ev.stopPropagation(); }; @@ -158,7 +162,7 @@ onMounted(() => { el.onpointermove = preventProp; pwsp.on("change", () => { - textBox.textContent = pwsp.currSlide.data.alt?.trim(); + textBox.textContent = pwsp.currSlide?.data.alt?.trim() ?? null; }); }, }); @@ -168,7 +172,7 @@ onMounted(() => { history.pushState(null, "", location.href); addEventListener("popstate", close); // This is a workaround. Not sure why, but when clicking to open, it doesn't move focus to the photoswipe. Preventing using esc to close. However when using keyboard to open it already focuses the lightbox fine. - lightbox.pswp.element.focus(); + lightbox.pswp?.element?.focus(); }); lightbox.on("close", () => { removeEventListener("popstate", close); @@ -180,7 +184,7 @@ onMounted(() => { function close() { removeEventListener("popstate", close); history.forward(); - lightbox.pswp.close(); + lightbox.pswp?.close(); } }); @@ -198,7 +202,7 @@ const isModule = (file: entities.DriveFile): boolean => { return ( FILE_TYPE_TRACKER_MODULES.includes(file.type) || FILE_EXT_TRACKER_MODULES.some((ext) => { - return file.name.toLowerCase().endsWith("." + ext); + return file.name.toLowerCase().endsWith(`.${ext}`); }) ); }; diff --git a/packages/client/src/components/MkMention.vue b/packages/client/src/components/MkMention.vue index c2c38d313e..f943997459 100644 --- a/packages/client/src/components/MkMention.vue +++ b/packages/client/src/components/MkMention.vue @@ -23,7 +23,6 @@ :href="url" target="_blank" rel="noopener" - :style="{ background: bgCss }" @click.stop > <span class="main"> @@ -54,7 +53,7 @@ const url = `/${canonical}`; const isMe = isSignedIn && `@${props.username}@${toUnicode(props.host)}`.toLowerCase() === - `@${me.username}@${toUnicode(localHost)}`.toLowerCase(); + `@${me!.username}@${toUnicode(localHost)}`.toLowerCase(); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue index 688b3ce3a4..ed788c4375 100644 --- a/packages/client/src/components/MkMenu.child.vue +++ b/packages/client/src/components/MkMenu.child.vue @@ -37,8 +37,8 @@ function setPosition() { const rect = props.targetElement.getBoundingClientRect(); const left = props.targetElement.offsetWidth; const top = rect.top - rootRect.top - 8; - el.value.style.left = left + "px"; - el.value.style.top = top + "px"; + el.value!.style.left = `${left}px`; + el.value!.style.top = `${top}px`; } function onChildClosed(actioned?: boolean) { @@ -58,7 +58,7 @@ onMounted(() => { defineExpose({ checkHit: (ev: MouseEvent) => { - return ev.target === el.value || el.value.contains(ev.target); + return ev.target === el.value || el.value?.contains(ev.target as Node); }, }); </script> diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue index 2a7db40be8..c778f0f6f1 100644 --- a/packages/client/src/components/MkMenu.vue +++ b/packages/client/src/components/MkMenu.vue @@ -89,7 +89,8 @@ ></span> </a> <button - v-else-if="item.type === 'user' && !items.hidden" + v-else-if="item.type === 'user'" + v-show="!item.hidden" class="_button item" :class="{ active: item.active }" :disabled="item.active" @@ -206,6 +207,7 @@ import { onMounted, ref, watch, + shallowRef, } from "vue"; import { FocusTrap } from "focus-trap-vue"; import FormSwitch from "@/components/form/switch.vue"; @@ -213,6 +215,7 @@ import type { InnerMenuItem, MenuAction, MenuItem, + MenuParent, MenuPending, } from "@/types/menu"; import * as os from "@/os"; @@ -234,20 +237,24 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (ev: "close", actioned?: boolean): void; + close: [actioned?: boolean]; }>(); const itemsEl = ref<HTMLDivElement>(); -const items2: InnerMenuItem[] = ref([]); +const items2 = shallowRef<InnerMenuItem[]>([]); const child = ref<InstanceType<typeof XChild>>(); const childShowingItem = ref<MenuItem | null>(); +// FIXME: this is not used +const isActive = ref(); + watch( () => props.items, () => { + // FIXME: what's this? const items: (MenuItem | MenuPending)[] = [...props.items].filter( (item) => item !== undefined, ); @@ -288,29 +295,29 @@ function onGlobalMousedown(event: MouseEvent) { if ( childTarget.value && (event.target === childTarget.value || - childTarget.value.contains(event.target)) + childTarget.value.contains(event.target as Node)) ) return; - if (child.value && child.value.checkHit(event)) return; + if (child.value?.checkHit(event)) return; closeChild(); } let childCloseTimer: null | number = null; -function onItemMouseEnter(item) { +function onItemMouseEnter(_item) { childCloseTimer = window.setTimeout(() => { closeChild(); }, 300); } -function onItemMouseLeave(item) { +function onItemMouseLeave(_item) { if (childCloseTimer) window.clearTimeout(childCloseTimer); } -async function showChildren(item: MenuItem, ev: MouseEvent) { +async function showChildren(item: MenuParent, ev: MouseEvent) { if (props.asDrawer) { - os.popupMenu(item.children, ev.currentTarget ?? ev.target); + os.popupMenu(item.children, (ev.currentTarget ?? ev.target) as HTMLElement); close(); } else { - childTarget.value = ev.currentTarget ?? ev.target; + childTarget.value = (ev.currentTarget ?? ev.target) as HTMLElement; childMenu.value = item.children; childShowingItem.value = item; } diff --git a/packages/client/src/components/MkMiniChart.vue b/packages/client/src/components/MkMiniChart.vue index bfa83f3d80..e82733c715 100644 --- a/packages/client/src/components/MkMiniChart.vue +++ b/packages/client/src/components/MkMiniChart.vue @@ -20,7 +20,7 @@ :stroke="color" stroke-width="2" /> - <circle :cx="headX" :cy="headY" r="3" :fill="color" /> + <circle :cx="headX ?? undefined" :cy="headY ?? undefined" r="3" :fill="color" /> </svg> </template> diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue index 6a2897cebf..57ecc1b0d8 100644 --- a/packages/client/src/components/MkModal.vue +++ b/packages/client/src/components/MkModal.vue @@ -108,8 +108,11 @@ type ModalTypes = "popup" | "dialog" | "dialog:top" | "drawer"; const props = withDefaults( defineProps<{ manualShowing?: boolean | null; - anchor?: { x: string; y: string }; - src?: HTMLElement; + anchor?: { + x: "left" | "center" | "right"; + y: "top" | "center" | "bottom"; + }; + src?: HTMLElement | null; preferType?: ModalTypes | "auto"; zPriority?: "low" | "middle" | "high"; noOverlap?: boolean; @@ -118,7 +121,7 @@ const props = withDefaults( }>(), { manualShowing: null, - src: undefined, + src: null, anchor: () => ({ x: "center", y: "bottom" }), preferType: "auto", zPriority: "low", @@ -139,6 +142,9 @@ const emit = defineEmits<{ provide("modal", true); +// FIXME: this may not used +const isActive = ref(); + const maxHeight = ref<number>(); const fixed = ref(false); const transformOrigin = ref("center"); @@ -189,7 +195,7 @@ const transitionDuration = computed(() => let contentClicking = false; -const focusedElement = document.activeElement; +const focusedElement = document.activeElement as HTMLElement; function close(_ev?, opts: { useSendAnimation?: boolean } = {}) { // removeEventListener("popstate", close); // if (props.preferType == "dialog") { @@ -204,7 +210,7 @@ function close(_ev?, opts: { useSendAnimation?: boolean } = {}) { showing.value = false; emit("close"); if (!props.noReturnFocus) { - focusedElement.focus(); + focusedElement?.focus(); } } @@ -235,8 +241,8 @@ const align = () => { const width = content.value!.offsetWidth; const height = content.value!.offsetHeight; - let left: number; - let top: number; + let left = 0; + let top = MARGIN; const x = srcRect.left + (fixed.value ? 0 : window.scrollX); const y = srcRect.top + (fixed.value ? 0 : window.scrollY); diff --git a/packages/client/src/components/MkPopupMenu.vue b/packages/client/src/components/MkPopupMenu.vue index 9a98648ea2..1bee69a0b2 100644 --- a/packages/client/src/components/MkPopupMenu.vue +++ b/packages/client/src/components/MkPopupMenu.vue @@ -35,7 +35,7 @@ defineProps<{ align?: "center" | string; width?: number; viaKeyboard?: boolean; - src?: any; + src?: HTMLElement | null; noReturnFocus?; }>(); diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 426fd29084..32d84396ef 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -213,7 +213,7 @@ interface VueComponentConstructor<P, E> { export async function popup<Props, Emits>( component: VueComponentConstructor<Props, Emits>, props: Props & Record<string, unknown>, - events: Partial<NonNullable<Emits>> | Record<string, never> = {}, + events: Partial<Emits> = {}, disposeEvent?: string, ) { markRaw(component); @@ -858,7 +858,7 @@ export async function openEmojiPicker( export function popupMenu( items: MenuItem[] | Ref<MenuItem[]>, - src?: HTMLElement, + src?: HTMLElement | null, options?: { align?: string; width?: number; From 2bf51abc126e01db2a90fb1c544016b639666b31 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 11:31:11 +0800 Subject: [PATCH 032/110] fix type of MkModalPageWindow --- .../client/src/components/MkDrive.file.vue | 4 ++-- .../client/src/components/MkLaunchPad.vue | 2 +- packages/client/src/components/MkModal.vue | 2 +- .../src/components/MkModalPageWindow.vue | 22 +++++++++++-------- packages/client/src/nirax.ts | 2 +- packages/client/src/os.ts | 17 +++++++------- packages/client/src/router.ts | 6 ++--- packages/client/src/scripts/page-metadata.ts | 1 + packages/firefish-js/src/api.types.ts | 6 ++--- 9 files changed, 33 insertions(+), 29 deletions(-) diff --git a/packages/client/src/components/MkDrive.file.vue b/packages/client/src/components/MkDrive.file.vue index 80a428adc3..6d348a33e7 100644 --- a/packages/client/src/components/MkDrive.file.vue +++ b/packages/client/src/components/MkDrive.file.vue @@ -182,8 +182,8 @@ function describe() { }, { done: (result: { - canceled: boolean, - result?: string | null, + canceled: boolean; + result?: string | null; }) => { if (!result || result.canceled) return; const comment = result.result; diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue index 084283c270..b3ca7bd34d 100644 --- a/packages/client/src/components/MkLaunchPad.vue +++ b/packages/client/src/components/MkLaunchPad.vue @@ -73,7 +73,7 @@ import { deviceKind } from "@/scripts/device-kind"; const props = withDefaults( defineProps<{ src?: HTMLElement; - anchor?: { + anchor?: { x: "left" | "center" | "right"; y: "top" | "center" | "bottom"; }; diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue index 57ecc1b0d8..2bed9f8295 100644 --- a/packages/client/src/components/MkModal.vue +++ b/packages/client/src/components/MkModal.vue @@ -108,7 +108,7 @@ type ModalTypes = "popup" | "dialog" | "dialog:top" | "drawer"; const props = withDefaults( defineProps<{ manualShowing?: boolean | null; - anchor?: { + anchor?: { x: "left" | "center" | "right"; y: "top" | "center" | "bottom"; }; diff --git a/packages/client/src/components/MkModalPageWindow.vue b/packages/client/src/components/MkModalPageWindow.vue index e9c8136ae4..a4dea9ee32 100644 --- a/packages/client/src/components/MkModalPageWindow.vue +++ b/packages/client/src/components/MkModalPageWindow.vue @@ -29,7 +29,7 @@ <button class="_button" :aria-label="i18n.ts.close" - @click="$refs.modal.close()" + @click="modal!.close()" > <i :class="icon('ph-x')"></i> </button> @@ -65,6 +65,7 @@ import type { PageMetadata } from "@/scripts/page-metadata"; import { provideMetadataReceiver } from "@/scripts/page-metadata"; import { Router } from "@/nirax"; import icon from "@/scripts/icon"; +import type { MenuItem } from "@/types/menu"; const props = defineProps<{ initialPath: string; @@ -81,11 +82,11 @@ router.addListener("push", (ctx) => {}); const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); const rootEl = ref(); -const modal = ref<InstanceType<typeof MkModal>>(); +const modal = ref<InstanceType<typeof MkModal> | null>(null); const path = ref(props.initialPath); const width = ref(860); const height = ref(660); -const history = []; +const history: string[] = []; provide("router", router); provideMetadataReceiver((info) => { @@ -95,7 +96,7 @@ provide("shouldOmitHeaderTitle", true); provide("shouldHeaderThin", true); const pageUrl = computed(() => url + path.value); -const contextmenu = computed(() => { +const contextmenu = computed((): MenuItem[] => { return [ { type: "label", @@ -117,7 +118,7 @@ const contextmenu = computed(() => { text: i18n.ts.openInNewTab, action: () => { window.open(pageUrl.value, "_blank"); - modal.value.close(); + modal.value!.close(); }, }, { @@ -130,23 +131,26 @@ const contextmenu = computed(() => { ]; }); -function navigate(path, record = true) { +function navigate(path: string, record = true) { if (record) history.push(router.getCurrentPath()); router.push(path); } function back() { - navigate(history.pop(), false); + const backTo = history.pop(); + if (backTo) { + navigate(backTo, false); + } } function expand() { mainRouter.push(path.value); - modal.value.close(); + modal.value!.close(); } function popout() { _popout(path.value, rootEl.value); - modal.value.close(); + modal.value!.close(); } function onContextmenu(ev: MouseEvent) { diff --git a/packages/client/src/nirax.ts b/packages/client/src/nirax.ts index f214060762..da162338b6 100644 --- a/packages/client/src/nirax.ts +++ b/packages/client/src/nirax.ts @@ -6,7 +6,7 @@ import { shallowRef } from "vue"; import { safeURIDecode } from "@/scripts/safe-uri-decode"; import { pleaseLogin } from "@/scripts/please-login"; -interface RouteDef { +export interface RouteDef { path: string; component: Component; query?: Record<string, string>; diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 32d84396ef..49d03b76cc 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -3,10 +3,7 @@ import { EventEmitter } from "eventemitter3"; import { type entities, api as firefishApi } from "firefish-js"; import insertTextAtCursor from "insert-text-at-cursor"; -import type { - Component, - Ref, -} from "vue"; +import type { Component, Ref } from "vue"; import { defineAsyncComponent, markRaw, ref } from "vue"; import { i18n } from "./i18n"; import MkDialog from "@/components/MkDialog.vue"; @@ -177,11 +174,13 @@ export function promiseDialog<T>( } let popupIdCount = 0; -export const popups = ref<{ - id: number; - component: Component; - props: Record<string, unknown>; -}[]>([]); +export const popups = ref< + { + id: number; + component: Component; + props: Record<string, unknown>; + }[] +>([]); const zIndexes = { low: 1000000, diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts index a21df2981e..7273919261 100644 --- a/packages/client/src/router.ts +++ b/packages/client/src/router.ts @@ -1,18 +1,18 @@ import type { AsyncComponentLoader } from "vue"; import { defineAsyncComponent, inject } from "vue"; import { isEmojiMod, isModerator, me } from "@/me"; -import { Router } from "@/nirax"; +import { type RouteDef, Router } from "@/nirax"; import MkError from "@/pages/_error_.vue"; import MkLoading from "@/pages/_loading_.vue"; -const page = (loader: AsyncComponentLoader<any>) => +const page = (loader: AsyncComponentLoader) => defineAsyncComponent({ loader, loadingComponent: MkLoading, errorComponent: MkError, }); -export const routes = [ +export const routes: RouteDef[] = [ { path: "/@:initUser/pages/:initPageName/view-source", component: page(() => import("./pages/page-editor/page-editor.vue")), diff --git a/packages/client/src/scripts/page-metadata.ts b/packages/client/src/scripts/page-metadata.ts index e2f470d0bb..cf8d7938a7 100644 --- a/packages/client/src/scripts/page-metadata.ts +++ b/packages/client/src/scripts/page-metadata.ts @@ -12,6 +12,7 @@ export interface PageMetadata { avatar?: entities.UserDetailed | null; userName?: entities.User | null; bg?: string; + hideHeader?: boolean; } export function definePageMetadata( diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts index 73e9fca69f..890e6bcd48 100644 --- a/packages/firefish-js/src/api.types.ts +++ b/packages/firefish-js/src/api.types.ts @@ -281,9 +281,9 @@ export type Endpoints = { "drive/files/attached-notes": { req: TODO; res: Note[] }; "drive/files/caption-image": { req: { - url: string, - } - res: string, + url: string; + }; + res: string; }; "drive/files/check-existence": { req: TODO; res: TODO }; "drive/files/create": { req: TODO; res: TODO }; From ea6ef881c21bb54297a02b3d3e013a44dd2330a7 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 14:08:20 +0800 Subject: [PATCH 033/110] fix types of components --- packages/client/src/components/MkModPlayer.vue | 7 ++++--- packages/client/src/components/MkModalWindow.vue | 7 +++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/client/src/components/MkModPlayer.vue b/packages/client/src/components/MkModPlayer.vue index cdaa5f7040..21eecbdf6d 100644 --- a/packages/client/src/components/MkModPlayer.vue +++ b/packages/client/src/components/MkModPlayer.vue @@ -140,7 +140,7 @@ const patternShow = ref(false); const modPattern = ref<HTMLDivElement>(); const progress = ref<typeof FormRange>(); const position = ref(0); -const patData = shallowRef([] as ModRow[][]); +const patData = shallowRef<readonly ModRow[][]>([]); const currentPattern = ref(0); const nbChannels = ref(0); const length = ref(1); @@ -159,7 +159,7 @@ function load() { error.value = false; fetching.value = false; }) - .catch((e: any) => { + .catch((e: unknown) => { console.error(e); error.value = true; fetching.value = false; @@ -293,12 +293,13 @@ function isRowActive(i: number) { } return true; } + return false; } function indexText(i: number) { let rowText = i.toString(16); if (rowText.length === 1) { - rowText = "0" + rowText; + rowText = `0${rowText}`; } return rowText; } diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue index c9d126ee90..6471bf1722 100644 --- a/packages/client/src/components/MkModalWindow.vue +++ b/packages/client/src/components/MkModalWindow.vue @@ -15,7 +15,7 @@ height: scroll ? height ? `${props.height}px` - : null + : undefined : height ? `min(${props.height}px, 100%)` : '100%', @@ -65,7 +65,7 @@ </template> <script lang="ts" setup> -import { shallowRef } from "vue"; +import { ref, shallowRef } from "vue"; import { FocusTrap } from "focus-trap-vue"; import MkModal from "./MkModal.vue"; @@ -96,6 +96,9 @@ const emit = defineEmits<{ (event: "ok"): void; }>(); +// FIXME: seems that this is not used +const isActive = ref(); + const modal = shallowRef<InstanceType<typeof MkModal>>(); const rootEl = shallowRef<HTMLElement>(); const headerEl = shallowRef<HTMLElement>(); From e4927c9b9bff924a7eefd48b920d9341757a9a00 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 16:37:32 +0800 Subject: [PATCH 034/110] fix types of components --- .../src/components/MkInstanceTicker.vue | 9 +-- packages/client/src/components/MkNote.vue | 62 +++++++++-------- .../client/src/components/MkNoteDetailed.vue | 66 ++++++++++--------- .../client/src/components/MkNotePreview.vue | 4 +- packages/client/src/components/MkNotes.vue | 14 +++- .../MkNotificationSettingWindow.vue | 6 +- .../src/components/MkNotificationToast.vue | 3 +- .../client/src/components/MkNotifications.vue | 6 +- .../client/src/components/MkPagination.vue | 26 +++++++- .../src/components/MkPostFormDialog.vue | 6 +- .../client/src/components/MkRenoteButton.vue | 6 +- .../src/components/MkSubNoteContent.vue | 8 +-- packages/client/src/components/MkUserList.vue | 15 +++-- .../components/MkUserSelectLocalDialog.vue | 10 +-- .../global/MkMisskeyFlavoredMarkdown.vue | 2 +- packages/client/src/components/mfm.ts | 2 +- packages/client/src/config.ts | 2 +- packages/client/src/os.ts | 32 +++++---- packages/client/src/pages/about.vue | 11 ++-- packages/client/src/pages/admin/abuses.vue | 6 +- packages/client/src/pages/admin/emojis.vue | 7 +- packages/client/src/pages/admin/users.vue | 8 ++- packages/client/src/pages/announcements.vue | 3 +- packages/client/src/pages/favorites.vue | 5 +- .../client/src/pages/follow-requests-sent.vue | 7 +- packages/client/src/pages/follow-requests.vue | 9 ++- .../src/pages/messaging/messaging-room.vue | 6 +- .../client/src/pages/my-antennas/index.vue | 4 +- packages/client/src/pages/my-clips/index.vue | 8 ++- packages/client/src/pages/my-lists/index.vue | 8 ++- packages/client/src/pages/note-history.vue | 8 ++- packages/client/src/pages/search.vue | 5 +- packages/client/src/scripts/get-note-menu.ts | 5 +- .../client/src/scripts/use-note-capture.ts | 2 +- packages/client/src/store.ts | 24 +++++-- packages/client/src/types/note.ts | 5 ++ packages/firefish-js/src/api.types.ts | 50 +++++++++++++- packages/firefish-js/src/entities.ts | 18 ++--- 38 files changed, 319 insertions(+), 159 deletions(-) diff --git a/packages/client/src/components/MkInstanceTicker.vue b/packages/client/src/components/MkInstanceTicker.vue index 498303ef8e..bbf999c183 100644 --- a/packages/client/src/components/MkInstanceTicker.vue +++ b/packages/client/src/components/MkInstanceTicker.vue @@ -20,15 +20,10 @@ import { ref } from "vue"; import { instanceName, version } from "@/config"; import { instance as Instance } from "@/instance"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; +import type { entities } from "firefish-js"; const props = defineProps<{ - instance?: { - faviconUrl?: string; - name: string; - themeColor?: string; - softwareName?: string; - softwareVersion?: string; - }; + instance?: entities.InstanceLite; }>(); const ticker = ref<HTMLElement | null>(null); diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue index 65dbd17d80..6a22652f60 100644 --- a/packages/client/src/components/MkNote.vue +++ b/packages/client/src/components/MkNote.vue @@ -9,7 +9,7 @@ v-vibrate="5" :aria-label="accessibleLabel" class="tkcbzcuz note-container" - :tabindex="!isDeleted ? '-1' : null" + :tabindex="!isDeleted ? '-1' : undefined" :class="{ renote: isRenote }" > <MkNoteSub @@ -112,9 +112,9 @@ :note="appearNote" :detailed="true" :detailed-view="detailedView" - :parent-id="appearNote.parentId" + :parent-id="appearNote.id" @push="(e) => router.push(notePage(e))" - @focusfooter="footerEl.focus()" + @focusfooter="footerEl!.focus()" @expanded="(e) => setPostExpanded(e)" ></MkSubNoteContent> <div v-if="translating || translation" class="translation"> @@ -312,11 +312,17 @@ import { notePage } from "@/filters/note"; import { deepClone } from "@/scripts/clone"; import { getNoteSummary } from "@/scripts/get-note-summary"; import icon from "@/scripts/icon"; +import type { NoteTranslation } from "@/types/note"; const router = useRouter(); +type NoteType = entities.Note & { + _featuredId_?: string; + _prId_?: string; +}; + const props = defineProps<{ - note: entities.Note; + note: NoteType; pinned?: boolean; detailedView?: boolean; collapsedReply?: boolean; @@ -354,18 +360,18 @@ const isRenote = note.value.fileIds.length === 0 && note.value.poll == null; -const el = ref<HTMLElement>(); +const el = ref<HTMLElement | null>(null); const footerEl = ref<HTMLElement>(); const menuButton = ref<HTMLElement>(); const starButton = ref<InstanceType<typeof XStarButton>>(); -const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); +const renoteButton = ref<InstanceType<typeof XRenoteButton> | null>(null); const renoteTime = ref<HTMLElement>(); -const reactButton = ref<HTMLElement>(); +const reactButton = ref<HTMLElement | null>(null); const appearNote = computed(() => - isRenote ? (note.value.renote as entities.Note) : note.value, + isRenote ? (note.value.renote as NoteType) : note.value, ); -const isMyRenote = isSignedIn && me.id === note.value.userId; -const showContent = ref(false); +const isMyRenote = isSignedIn && me!.id === note.value.userId; +// const showContent = ref(false); const isDeleted = ref(false); const muted = ref( getWordSoftMute( @@ -375,7 +381,7 @@ const muted = ref( defaultStore.state.mutedLangs, ), ); -const translation = ref(null); +const translation = ref<NoteTranslation | null>(null); const translating = ref(false); const enableEmojiReactions = defaultStore.state.enableEmojiReactions; const expandOnNoteClick = defaultStore.state.expandOnNoteClick; @@ -391,7 +397,7 @@ const isForeignLanguage: boolean = return postLang !== "" && postLang !== targetLang; })(); -async function translate_(noteId, targetLang: string) { +async function translate_(noteId: string, targetLang: string) { return await os.api("notes/translate", { noteId, targetLang, @@ -421,12 +427,13 @@ async function translate() { const keymap = { r: () => reply(true), "e|a|plus": () => react(true), - q: () => renoteButton.value.renote(true), + q: () => renoteButton.value!.renote(true), "up|k": focusBefore, "down|j": focusAfter, esc: blur, "m|o": () => menu(true), - s: () => showContent.value !== showContent.value, + // FIXME: What's this? + // s: () => showContent.value !== showContent.value, }; if (appearNote.value.historyId == null) { @@ -437,12 +444,12 @@ if (appearNote.value.historyId == null) { }); } -function reply(viaKeyboard = false): void { +function reply(_viaKeyboard = false): void { pleaseLogin(); os.post( { reply: appearNote.value, - animation: !viaKeyboard, + // animation: !viaKeyboard, }, () => { focus(); @@ -450,11 +457,11 @@ function reply(viaKeyboard = false): void { ); } -function react(viaKeyboard = false): void { +function react(_viaKeyboard = false): void { pleaseLogin(); blur(); reactionPicker.show( - reactButton.value, + reactButton.value!, (reaction) => { os.api("notes/reactions/create", { noteId: appearNote.value.id, @@ -467,7 +474,7 @@ function react(viaKeyboard = false): void { ); } -function undoReact(note): void { +function undoReact(note: NoteType): void { const oldReaction = note.myReaction; if (!oldReaction) return; os.api("notes/reactions/delete", { @@ -481,16 +488,17 @@ const currentClipPage = inject<Ref<entities.Clip> | null>( ); function onContextmenu(ev: MouseEvent): void { - const isLink = (el: HTMLElement) => { + const isLink = (el: HTMLElement): boolean => { if (el.tagName === "A") return true; // The Audio element's context menu is the browser default, such as for selecting playback speed. if (el.tagName === "AUDIO") return true; if (el.parentElement) { return isLink(el.parentElement); } + return false; }; - if (isLink(ev.target)) return; - if (window.getSelection().toString() !== "") return; + if (isLink(ev.target as HTMLElement)) return; + if (window.getSelection()?.toString() !== "") return; if (defaultStore.state.useReactionPickerForContextMenu) { ev.preventDefault(); @@ -509,7 +517,7 @@ function onContextmenu(ev: MouseEvent): void { os.pageWindow(notePage(appearNote.value)); }, }, - notePage(appearNote.value) != location.pathname + notePage(appearNote.value) !== location.pathname ? { icon: `${icon("ph-arrows-out-simple")}`, text: i18n.ts.showInPage, @@ -589,11 +597,11 @@ function showRenoteMenu(viaKeyboard = false): void { } function focus() { - el.value.focus(); + el.value!.focus(); } function blur() { - el.value.blur(); + el.value!.blur(); } function focusBefore() { @@ -605,12 +613,12 @@ function focusAfter() { } function scrollIntoView() { - el.value.scrollIntoView(); + el.value!.scrollIntoView(); } function noteClick(e) { if ( - document.getSelection().type === "Range" || + document.getSelection()?.type === "Range" || props.detailedView || !expandOnNoteClick ) { diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue index d17e21b2b1..b6c0d784ba 100644 --- a/packages/client/src/components/MkNoteDetailed.vue +++ b/packages/client/src/components/MkNoteDetailed.vue @@ -6,7 +6,7 @@ v-hotkey="keymap" v-size="{ max: [500, 350, 300] }" class="lxwezrsl _block" - :tabindex="!isDeleted ? '-1' : null" + :tabindex="!isDeleted ? '-1' : undefined" :class="{ renote: isRenote }" > <MkNoteSub @@ -64,7 +64,7 @@ ) }} </option> - <option v-if="directQuotes?.length > 0" value="quotes"> + <option v-if="directQuotes && directQuotes.length > 0" value="quotes"> <!-- <i :class="icon('ph-quotes')"></i> --> {{ wordWithCount( @@ -102,7 +102,7 @@ :detailed-view="true" :parent-id="note.id" /> - <MkLoading v-else-if="tab === 'quotes' && directQuotes.length > 0" /> + <MkLoading v-else-if="tab === 'quotes' && directQuotes && directQuotes.length > 0" /> <!-- <MkPagination v-if="tab === 'renotes'" @@ -225,12 +225,12 @@ if (noteViewInterruptors.length > 0) { }); } -const el = ref<HTMLElement>(); +const el = ref<HTMLElement | null>(null); const noteEl = ref(); const menuButton = ref<HTMLElement>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const reactButton = ref<HTMLElement>(); -const showContent = ref(false); +// const showContent = ref(false); const isDeleted = ref(false); const muted = ref( getWordSoftMute( @@ -248,7 +248,8 @@ const directReplies = ref<null | entities.Note[]>([]); const directQuotes = ref<null | entities.Note[]>([]); const clips = ref(); const renotes = ref(); -let isScrolling; +const isRenote = ref(note.value.renoteId != null); +let isScrolling: boolean; const reactionsCount = Object.values(props.note.reactions).reduce( (x, y) => x + y, @@ -258,10 +259,10 @@ const reactionsCount = Object.values(props.note.reactions).reduce( const keymap = { r: () => reply(true), "e|a|plus": () => react(true), - q: () => renoteButton.value.renote(true), + q: () => renoteButton.value!.renote(true), esc: blur, "m|o": () => menu(true), - s: () => showContent.value !== showContent.value, + // s: () => showContent.value !== showContent.value, }; useNoteCapture({ @@ -270,21 +271,21 @@ useNoteCapture({ isDeletedRef: isDeleted, }); -function reply(viaKeyboard = false): void { +function reply(_viaKeyboard = false): void { pleaseLogin(); os.post({ reply: note.value, - animation: !viaKeyboard, + // animation: !viaKeyboard, }).then(() => { focus(); }); } -function react(viaKeyboard = false): void { +function react(_viaKeyboard = false): void { pleaseLogin(); blur(); reactionPicker.show( - reactButton.value, + reactButton.value!, (reaction) => { os.api("notes/reactions/create", { noteId: note.value.id, @@ -297,13 +298,13 @@ function react(viaKeyboard = false): void { ); } -function undoReact(note): void { - const oldReaction = note.myReaction; - if (!oldReaction) return; - os.api("notes/reactions/delete", { - noteId: note.id, - }); -} +// function undoReact(note): void { +// const oldReaction = note.myReaction; +// if (!oldReaction) return; +// os.api("notes/reactions/delete", { +// noteId: note.id, +// }); +// } function onContextmenu(ev: MouseEvent): void { const isLink = (el: HTMLElement) => { @@ -312,8 +313,8 @@ function onContextmenu(ev: MouseEvent): void { return isLink(el.parentElement); } }; - if (isLink(ev.target)) return; - if (window.getSelection().toString() !== "") return; + if (isLink(ev.target as HTMLElement)) return; + if (window.getSelection()?.toString() !== "") return; if (defaultStore.state.useReactionPickerForContextMenu) { ev.preventDefault(); @@ -362,12 +363,17 @@ os.api("notes/children", { limit: 30, depth: 12, }).then((res) => { - res = res.reduce((acc, resNote) => { - if (resNote.userId == note.value.userId) { - return [...acc, resNote]; - } - return [resNote, ...acc]; - }, []); + // biome-ignore lint/style/noParameterAssign: assign it intentially + res = res + .filter((n) => n.userId !== note.value.userId) + .reverse() + .concat(res.filter((n) => n.userId === note.value.userId)); + // res = res.reduce((acc: entities.Note[], resNote) => { + // if (resNote.userId === note.value.userId) { + // return [...acc, resNote]; + // } + // return [resNote, ...acc]; + // }, []); replies.value = res; directReplies.value = res .filter((resNote) => resNote.replyId === note.value.id) @@ -438,7 +444,7 @@ async function onNoteUpdated( } switch (type) { - case "replied": + case "replied": { const { id: createdId } = body; const replyNote = await os.api("notes/show", { noteId: createdId, @@ -446,10 +452,10 @@ async function onNoteUpdated( replies.value.splice(found, 0, replyNote); if (found === 0) { - directReplies.value.push(replyNote); + directReplies.value!.push(replyNote); } break; - + } case "deleted": if (found === 0) { isDeleted.value = true; diff --git a/packages/client/src/components/MkNotePreview.vue b/packages/client/src/components/MkNotePreview.vue index 454936dfbe..9ba8c0025e 100644 --- a/packages/client/src/components/MkNotePreview.vue +++ b/packages/client/src/components/MkNotePreview.vue @@ -1,9 +1,9 @@ <template> <div v-size="{ min: [350, 500] }" class="fefdfafb"> - <MkAvatar class="avatar" :user="me" disable-link /> + <MkAvatar class="avatar" :user="me!" disable-link /> <div class="main"> <div class="header"> - <MkUserName :user="me" /> + <MkUserName :user="me!" /> </div> <div class="body"> <div class="content"> diff --git a/packages/client/src/components/MkNotes.vue b/packages/client/src/components/MkNotes.vue index 2351cd6e47..b5fdc95fbf 100644 --- a/packages/client/src/components/MkNotes.vue +++ b/packages/client/src/components/MkNotes.vue @@ -40,7 +40,11 @@ <script lang="ts" setup> import { ref } from "vue"; -import type { PagingOf } from "@/components/MkPagination.vue"; +import type { + MkPaginationType, + PagingKeyOf, + PagingOf, +} from "@/components/MkPagination.vue"; import XNote from "@/components/MkNote.vue"; import XList from "@/components/MkDateSeparatedList.vue"; import MkPagination from "@/components/MkPagination.vue"; @@ -56,10 +60,14 @@ defineProps<{ disableAutoLoad?: boolean; }>(); -const pagingComponent = ref<InstanceType<typeof MkPagination>>(); +const pagingComponent = ref<MkPaginationType< + PagingKeyOf<entities.Note> +> | null>(null); function scrollTop() { - scroll(tlEl.value, { top: 0, behavior: "smooth" }); + if (tlEl.value) { + scroll(tlEl.value, { top: 0, behavior: "smooth" }); + } } defineExpose({ diff --git a/packages/client/src/components/MkNotificationSettingWindow.vue b/packages/client/src/components/MkNotificationSettingWindow.vue index fac382f68c..20c3b43cb3 100644 --- a/packages/client/src/components/MkNotificationSettingWindow.vue +++ b/packages/client/src/components/MkNotificationSettingWindow.vue @@ -6,7 +6,7 @@ :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" - @close="dialog.close()" + @close="dialog!.close()" @closed="emit('closed')" > <template #header>{{ i18n.ts.notificationSetting }}</template> @@ -68,7 +68,7 @@ const includingTypes = computed(() => props.includingTypes || []); const dialog = ref<InstanceType<typeof XModalWindow>>(); -const typesMap = ref<Record<(typeof notificationTypes)[number], boolean>>({}); +const typesMap = ref({} as Record<(typeof notificationTypes)[number], boolean>); const useGlobalSetting = ref( (includingTypes.value === null || includingTypes.value.length === 0) && props.showGlobalToggle, @@ -89,7 +89,7 @@ function ok() { }); } - dialog.value.close(); + dialog.value!.close(); } function disableAll() { diff --git a/packages/client/src/components/MkNotificationToast.vue b/packages/client/src/components/MkNotificationToast.vue index c5ca37d36a..a18a7e3d48 100644 --- a/packages/client/src/components/MkNotificationToast.vue +++ b/packages/client/src/components/MkNotificationToast.vue @@ -19,9 +19,10 @@ import { onMounted, ref } from "vue"; import XNotification from "@/components/MkNotification.vue"; import * as os from "@/os"; import { defaultStore } from "@/store"; +import type { entities } from "firefish-js"; defineProps<{ - notification: any; // TODO + notification: entities.Notification; }>(); const emit = defineEmits<{ diff --git a/packages/client/src/components/MkNotifications.vue b/packages/client/src/components/MkNotifications.vue index 7b63b34197..43669c6607 100644 --- a/packages/client/src/components/MkNotifications.vue +++ b/packages/client/src/components/MkNotifications.vue @@ -44,7 +44,9 @@ <script lang="ts" setup> import { computed, onMounted, onUnmounted, ref } from "vue"; import type { StreamTypes, entities, notificationTypes } from "firefish-js"; -import MkPagination from "@/components/MkPagination.vue"; +import MkPagination, { + type MkPaginationType, +} from "@/components/MkPagination.vue"; import XNotification from "@/components/MkNotification.vue"; import XList from "@/components/MkDateSeparatedList.vue"; import XNote from "@/components/MkNote.vue"; @@ -59,7 +61,7 @@ const props = defineProps<{ const stream = useStream(); -const pagingComponent = ref<InstanceType<typeof MkPagination>>(); +const pagingComponent = ref<MkPaginationType<"i/notifications"> | null>(null); const pagination = { endpoint: "i/notifications" as const, diff --git a/packages/client/src/components/MkPagination.vue b/packages/client/src/components/MkPagination.vue index 143e3e1656..03a1f0e35f 100644 --- a/packages/client/src/components/MkPagination.vue +++ b/packages/client/src/components/MkPagination.vue @@ -67,7 +67,7 @@ </template> <script lang="ts" setup generic="E extends PagingKey"> -import type { ComputedRef } from "vue"; +import type { ComponentPublicInstance, ComputedRef } from "vue"; import { computed, isRef, onActivated, onDeactivated, ref, watch } from "vue"; import type { Endpoints, TypeUtils } from "firefish-js"; import * as os from "@/os"; @@ -81,8 +81,30 @@ import MkButton from "@/components/MkButton.vue"; import { i18n } from "@/i18n"; import { defaultStore } from "@/store"; +/** + * ref type of MkPagination<E> + * Due to Vue's incomplete type support for generic components, + * we have to manually maintain this type instead of + * using `InstanceType<typeof MkPagination>` + */ +export type MkPaginationType< + E extends PagingKey, + Item = Endpoints[E]["res"][number], +> = ComponentPublicInstance & { + items: Item[]; + queue: Item[]; + backed: boolean; + reload: () => Promise<void>; + refresh: () => Promise<void>; + prepend: (item: Item) => Promise<void>; + append: (item: Item) => Promise<void>; + removeItem: (finder: (item: Item) => boolean) => boolean; + updateItem: (id: string, replacer: (old: Item) => Item) => boolean; +}; + +export type PagingKeyOf<T> = TypeUtils.EndpointsOf<T[]>; // biome-ignore lint/suspicious/noExplicitAny: Used Intentionally -export type PagingKey = TypeUtils.EndpointsOf<any[]>; +export type PagingKey = PagingKeyOf<any>; export interface Paging<E extends PagingKey = PagingKey> { endpoint: E; diff --git a/packages/client/src/components/MkPostFormDialog.vue b/packages/client/src/components/MkPostFormDialog.vue index 380312ee13..cf6f45e5ed 100644 --- a/packages/client/src/components/MkPostFormDialog.vue +++ b/packages/client/src/components/MkPostFormDialog.vue @@ -2,7 +2,7 @@ <MkModal ref="modal" :prefer-type="'dialog'" - @click="modal.close()" + @click="modal!.close()" @closed="onModalClosed()" > <MkPostForm @@ -12,8 +12,8 @@ autofocus freeze-after-posted @posted="onPosted" - @cancel="modal.close()" - @esc="modal.close()" + @cancel="modal!.close()" + @esc="modal!.close()" /> </MkModal> </template> diff --git a/packages/client/src/components/MkRenoteButton.vue b/packages/client/src/components/MkRenoteButton.vue index 72a0559a0e..845298b89d 100644 --- a/packages/client/src/components/MkRenoteButton.vue +++ b/packages/client/src/components/MkRenoteButton.vue @@ -77,7 +77,7 @@ const hasRenotedBefore = ref(false); if (isSignedIn) { os.api("notes/renotes", { noteId: props.note.id, - userId: me.id, + userId: me!.id, limit: 1, }).then((res) => { hasRenotedBefore.value = res.length > 0; @@ -251,6 +251,10 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => { os.popupMenu(buttonActions, buttonRef.value, { viaKeyboard }); }; + +defineExpose({ + renote, +}); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue index 3c1e2a418b..7d350085bb 100644 --- a/packages/client/src/components/MkSubNoteContent.vue +++ b/packages/client/src/components/MkSubNoteContent.vue @@ -31,7 +31,6 @@ :text="note.cw" :author="note.user" :lang="note.lang" - :i="me" :custom-emojis="note.emojis" /> </p> @@ -63,8 +62,8 @@ <div class="body" v-bind="{ - 'aria-hidden': note.cw && !showContent ? 'true' : null, - tabindex: !showContent ? '-1' : null, + 'aria-hidden': note.cw && !showContent ? 'true' : undefined, + tabindex: !showContent ? '-1' : undefined, }" > <span v-if="note.deletedAt" style="opacity: 0.5" @@ -103,7 +102,6 @@ v-if="note.text" :text="note.text" :author="note.user" - :i="me" :lang="note.lang" :custom-emojis="note.emojis" /> @@ -256,7 +254,7 @@ async function toggleMfm() { } function focusFooter(ev) { - if (ev.key == "Tab" && !ev.getModifierState("Shift")) { + if (ev.key === "Tab" && !ev.getModifierState("Shift")) { emit("focusfooter"); } } diff --git a/packages/client/src/components/MkUserList.vue b/packages/client/src/components/MkUserList.vue index 92eb5df953..5043b920ff 100644 --- a/packages/client/src/components/MkUserList.vue +++ b/packages/client/src/components/MkUserList.vue @@ -11,10 +11,10 @@ </div> </template> - <template #default="{ items: users }"> + <template #default="{ items }: { items: entities.UserDetailed[] }"> <div class="efvhhmdq"> <MkUserInfo - v-for="user in users" + v-for="user in items" :key="user.id" class="user" :user="user" @@ -27,16 +27,21 @@ <script lang="ts" setup> import { ref } from "vue"; import MkUserInfo from "@/components/MkUserInfo.vue"; -import type { Paging } from "@/components/MkPagination.vue"; +import type { + MkPaginationType, + PagingKeyOf, + PagingOf, +} from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue"; import { i18n } from "@/i18n"; +import type { entities } from "firefish-js"; defineProps<{ - pagination: Paging; + pagination: PagingOf<entities.UserDetailed>; noGap?: boolean; }>(); -const pagingComponent = ref<InstanceType<typeof MkPagination>>(); +const pagingComponent = ref<MkPaginationType<PagingKeyOf<entities.User>>>(); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/MkUserSelectLocalDialog.vue b/packages/client/src/components/MkUserSelectLocalDialog.vue index bd8494cb7a..1a8aff3c83 100644 --- a/packages/client/src/components/MkUserSelectLocalDialog.vue +++ b/packages/client/src/components/MkUserSelectLocalDialog.vue @@ -94,9 +94,9 @@ import { defaultStore } from "@/store"; import { i18n } from "@/i18n"; const emit = defineEmits<{ - (ev: "ok", selected: entities.UserDetailed): void; - (ev: "cancel"): void; - (ev: "closed"): void; + ok: [selected: entities.UserDetailed]; + cancel: []; + closed: []; }>(); const username = ref(""); @@ -114,7 +114,7 @@ const search = () => { query: username.value, origin: "local", limit: 10, - detail: false, + detail: true, }).then((_users) => { users.value = _users; }); @@ -127,7 +127,7 @@ const ok = () => { // 最近使ったユーザー更新 let recents = defaultStore.state.recentlyUsedUsers; - recents = recents.filter((x) => x !== selected.value.id); + recents = recents.filter((x) => x !== selected.value!.id); recents.unshift(selected.value.id); defaultStore.set("recentlyUsedUsers", recents.splice(0, 16)); }; diff --git a/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue b/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue index 3fe805b5b4..0dc8a40617 100644 --- a/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue +++ b/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue @@ -26,7 +26,7 @@ withDefaults( text: string; plain?: boolean; nowrap?: boolean; - author?: entities.User; + author?: entities.User | null; customEmojis?: entities.EmojiLite[]; isNote?: boolean; advancedMfm?: boolean; diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts index f2b100a207..0b31b29bfb 100644 --- a/packages/client/src/components/mfm.ts +++ b/packages/client/src/components/mfm.ts @@ -30,7 +30,7 @@ export default defineComponent({ default: false, }, author: { - type: Object as PropType<entities.User>, + type: Object as PropType<entities.User | null>, default: null, }, // TODO: This variable is not used in the code and may be removed diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index e77ee87e85..1f9d6cdc7a 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -13,7 +13,7 @@ export const wsUrl = `${url export const lang = localStorage.getItem("lang"); export const langs = _LANGS_; export const locale = JSON.parse(localStorage.getItem("locale") || "en-US"); -export const version = _VERSION_; +export const version: string = _VERSION_; export const instanceName = siteName === "Firefish" ? host : siteName; export const ui = localStorage.getItem("ui"); export const debug = localStorage.getItem("debug") === "true"; diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 49d03b76cc..04c658ec89 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -22,7 +22,7 @@ const apiClient = new firefishApi.APIClient({ export const api = (( endpoint: string, - data: Record<string, any> = {}, + data: Record<string, unknown> = {}, token?: string | null | undefined, useToken = true, ) => { @@ -174,13 +174,14 @@ export function promiseDialog<T>( } let popupIdCount = 0; -export const popups = ref< - { - id: number; - component: Component; - props: Record<string, unknown>; - }[] ->([]); + +type PopupType = { + id: number; + component: Component; + props: Record<string, unknown>; + events: Record<string, unknown>; +}; +export const popups = ref<PopupType[]>([]); const zIndexes = { low: 1000000, @@ -922,18 +923,27 @@ export function contextMenu( }); } -export function post(props: Record<string, any> = {}) { - return new Promise((resolve, reject) => { +export function post( + props: InstanceType<typeof MkPostFormDialog>["$props"] = {}, + onClosed?: () => void, +) { + return new Promise<void>((resolve, reject) => { // NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない // NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、 // Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、 // 複数のpost formを開いたときに場合によってはエラーになる // もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが - let dispose; + // NOTE: Text area cannot be auto-focused on iOS when dynamically importing MkPostFormDialog + // NOTE: However, if you do not dynamically import, the MkPostFormDialog instance will be reused, + // Due to the effect that Vue internally creates a property called __props on the passed component, + // Sometimes an error occurs when opening multiple post forms + // Of course, opening multiple post forms is itself a bug on Misskey's side. + let dispose: () => void; popup(MkPostFormDialog, props, { closed: () => { resolve(); dispose(); + onClosed?.(); }, }).then((res) => { dispose = res.dispose; diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue index caf70856af..f757d3a1a5 100644 --- a/packages/client/src/pages/about.vue +++ b/packages/client/src/pages/about.vue @@ -176,6 +176,7 @@ import { computed, onMounted, ref, watch } from "vue"; import { Virtual } from "swiper/modules"; import { Swiper, SwiperSlide } from "swiper/vue"; +import type { Swiper as SwiperType } from "swiper/types"; import XEmojis from "./about.emojis.vue"; import XFederation from "./about.federation.vue"; import { host, version } from "@/config"; @@ -294,19 +295,19 @@ watch(iconSrc, (newValue, oldValue) => { } }); -let swiperRef = null; +let swiperRef: SwiperType | null = null; -function setSwiperRef(swiper) { +function setSwiperRef(swiper: SwiperType) { swiperRef = swiper; syncSlide(tabs.indexOf(tab.value)); } function onSlideChange() { - tab.value = tabs[swiperRef.activeIndex]; + tab.value = tabs[swiperRef!.activeIndex]; } -function syncSlide(index) { - swiperRef.slideTo(index); +function syncSlide(index: number) { + swiperRef!.slideTo(index); } </script> diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue index f94e24cf7c..72850ed478 100644 --- a/packages/client/src/pages/admin/abuses.vue +++ b/packages/client/src/pages/admin/abuses.vue @@ -94,14 +94,16 @@ import { computed, ref } from "vue"; import MkSelect from "@/components/form/select.vue"; -import MkPagination from "@/components/MkPagination.vue"; +import MkPagination, { + type MkPaginationType, +} from "@/components/MkPagination.vue"; import XAbuseReport from "@/components/MkAbuseReport.vue"; import { i18n } from "@/i18n"; import { definePageMetadata } from "@/scripts/page-metadata"; import icon from "@/scripts/icon"; import type { entities } from "firefish-js"; -const reports = ref<InstanceType<typeof MkPagination>>(); +const reports = ref<MkPaginationType<typeof pagination.endpoint> | null>(null); const state = ref("unresolved"); const reporterOrigin = ref<entities.OriginType>("combined"); diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue index 7802e5aad6..869979063c 100644 --- a/packages/client/src/pages/admin/emojis.vue +++ b/packages/client/src/pages/admin/emojis.vue @@ -153,7 +153,9 @@ import { computed, defineAsyncComponent, ref } from "vue"; import MkButton from "@/components/MkButton.vue"; import MkInput from "@/components/form/input.vue"; -import MkPagination from "@/components/MkPagination.vue"; +import MkPagination, { + type MkPaginationType, +} from "@/components/MkPagination.vue"; import MkSwitch from "@/components/form/switch.vue"; import FormSplit from "@/components/form/split.vue"; import { selectFile, selectFiles } from "@/scripts/select-file"; @@ -162,7 +164,8 @@ import { i18n } from "@/i18n"; import { definePageMetadata } from "@/scripts/page-metadata"; import icon from "@/scripts/icon"; -const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>(); +const emojisPaginationComponent = + ref<MkPaginationType<"admin/emoji/list"> | null>(null); const tab = ref("local"); const query = ref(null); diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue index d5028993e5..f72067b84e 100644 --- a/packages/client/src/pages/admin/users.vue +++ b/packages/client/src/pages/admin/users.vue @@ -126,7 +126,9 @@ import { computed, ref } from "vue"; import MkInput from "@/components/form/input.vue"; import MkSelect from "@/components/form/select.vue"; -import MkPagination from "@/components/MkPagination.vue"; +import MkPagination, { + type MkPaginationType, +} from "@/components/MkPagination.vue"; import * as os from "@/os"; import { lookupUser } from "@/scripts/lookup-user"; import { i18n } from "@/i18n"; @@ -134,7 +136,9 @@ import { definePageMetadata } from "@/scripts/page-metadata"; import MkUserCardMini from "@/components/MkUserCardMini.vue"; import icon from "@/scripts/icon"; -const paginationComponent = ref<InstanceType<typeof MkPagination>>(); +const paginationComponent = ref<MkPaginationType< + typeof pagination.endpoint +> | null>(null); const sort = ref("+createdAt"); const state = ref("all"); diff --git a/packages/client/src/pages/announcements.vue b/packages/client/src/pages/announcements.vue index df2a794ba4..59b703a35e 100644 --- a/packages/client/src/pages/announcements.vue +++ b/packages/client/src/pages/announcements.vue @@ -54,6 +54,7 @@ <script lang="ts" setup> import { computed, ref } from "vue"; import MkPagination from "@/components/MkPagination.vue"; +import type { MkPaginationType } from "@/components/MkPagination.vue"; import MkButton from "@/components/MkButton.vue"; import * as os from "@/os"; import { i18n } from "@/i18n"; @@ -66,7 +67,7 @@ const pagination = { limit: 10, }; -const paginationEl = ref<InstanceType<typeof MkPagination>>(); +const paginationEl = ref<MkPaginationType<"announcements"> | null>(null); function read(id: string) { if (!paginationEl.value) return; paginationEl.value.updateItem(id, (announcement) => { diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue index f9fb36f296..09e857449b 100644 --- a/packages/client/src/pages/favorites.vue +++ b/packages/client/src/pages/favorites.vue @@ -37,6 +37,7 @@ <script lang="ts" setup> import { ref } from "vue"; import MkPagination from "@/components/MkPagination.vue"; +import type { MkPaginationType } from "@/components/MkPagination.vue"; import XNote from "@/components/MkNote.vue"; import XList from "@/components/MkDateSeparatedList.vue"; import { i18n } from "@/i18n"; @@ -48,7 +49,9 @@ const pagination = { limit: 10, }; -const pagingComponent = ref<InstanceType<typeof MkPagination>>(); +const pagingComponent = ref<MkPaginationType< + typeof pagination.endpoint +> | null>(null); definePageMetadata({ title: i18n.ts.favorites, diff --git a/packages/client/src/pages/follow-requests-sent.vue b/packages/client/src/pages/follow-requests-sent.vue index 8ca769848b..b360c704f0 100644 --- a/packages/client/src/pages/follow-requests-sent.vue +++ b/packages/client/src/pages/follow-requests-sent.vue @@ -66,14 +66,17 @@ import { computed, ref } from "vue"; import { acct } from "firefish-js"; import MkPagination from "@/components/MkPagination.vue"; +import type { MkPaginationType } from "@/components/MkPagination.vue"; import { userPage } from "@/filters/user"; -import * as os from "@/os"; +// import * as os from "@/os"; import { i18n } from "@/i18n"; import { definePageMetadata } from "@/scripts/page-metadata"; import { me } from "@/me"; import icon from "@/scripts/icon"; -const paginationComponent = ref<InstanceType<typeof MkPagination>>(); +const paginationComponent = ref<MkPaginationType< + typeof pagination.endpoint +> | null>(null); const pagination = { endpoint: "following/requests/sent" as const, diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue index d4999458c8..3abc414c0c 100644 --- a/packages/client/src/pages/follow-requests.vue +++ b/packages/client/src/pages/follow-requests.vue @@ -85,6 +85,7 @@ import { computed, ref } from "vue"; import { acct } from "firefish-js"; import MkPagination from "@/components/MkPagination.vue"; +import type { MkPaginationType } from "@/components/MkPagination.vue"; import { userPage } from "@/filters/user"; import * as os from "@/os"; import { i18n } from "@/i18n"; @@ -92,7 +93,9 @@ import { definePageMetadata } from "@/scripts/page-metadata"; import { me } from "@/me"; import icon from "@/scripts/icon"; -const paginationComponent = ref<InstanceType<typeof MkPagination>>(); +const paginationComponent = ref<MkPaginationType< + typeof pagination.endpoint +> | null>(null); const pagination = { endpoint: "following/requests/list" as const, @@ -102,13 +105,13 @@ const pagination = { function accept(user) { os.api("following/requests/accept", { userId: user.id }).then(() => { - paginationComponent.value.reload(); + paginationComponent.value!.reload(); }); } function reject(user) { os.api("following/requests/reject", { userId: user.id }).then(() => { - paginationComponent.value.reload(); + paginationComponent.value!.reload(); }); } diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue index fa61ec4bc3..bde0003f2f 100644 --- a/packages/client/src/pages/messaging/messaging-room.vue +++ b/packages/client/src/pages/messaging/messaging-room.vue @@ -110,7 +110,7 @@ import { acct } from "firefish-js"; import XMessage from "./messaging-room.message.vue"; import XForm from "./messaging-room.form.vue"; import XList from "@/components/MkDateSeparatedList.vue"; -import type { Paging } from "@/components/MkPagination.vue"; +import type { MkPaginationType, Paging } from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue"; import { isBottomVisible, @@ -136,7 +136,9 @@ const stream = useStream(); const rootEl = ref<HTMLDivElement>(); const formEl = ref<InstanceType<typeof XForm>>(); -const pagingComponent = ref<InstanceType<typeof MkPagination>>(); +const pagingComponent = ref<MkPaginationType<"messaging/messages"> | null>( + null, +); const fetching = ref(true); const user = ref<entities.UserDetailed | null>(null); diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue index a4787f7241..5b645479a9 100644 --- a/packages/client/src/pages/my-antennas/index.vue +++ b/packages/client/src/pages/my-antennas/index.vue @@ -54,7 +54,7 @@ <script lang="ts" setup> import { computed, onActivated, onDeactivated, ref } from "vue"; -import MkPagination from "@/components/MkPagination.vue"; +import MkPagination, { MkPaginationType } from "@/components/MkPagination.vue"; import MkButton from "@/components/MkButton.vue"; import MkInfo from "@/components/MkInfo.vue"; import { i18n } from "@/i18n"; @@ -70,7 +70,7 @@ const headerActions = computed(() => []); const headerTabs = computed(() => []); -const list = ref<typeof MkPagination | null>(null); +const list = ref<MkPaginationType<typeof pagination.endpoint> | null>(null); let isCached = false; let refreshTimer: number | null = null; diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue index daaef6f3a0..d4eeb9e4a9 100644 --- a/packages/client/src/pages/my-clips/index.vue +++ b/packages/client/src/pages/my-clips/index.vue @@ -40,7 +40,9 @@ <script lang="ts" setup> import { computed, ref } from "vue"; -import MkPagination from "@/components/MkPagination.vue"; +import MkPagination, { + type MkPaginationType, +} from "@/components/MkPagination.vue"; import MkInfo from "@/components/MkInfo.vue"; import * as os from "@/os"; import { i18n } from "@/i18n"; @@ -52,7 +54,9 @@ const pagination = { limit: 10, }; -const pagingComponent = ref<InstanceType<typeof MkPagination>>(); +const pagingComponent = ref<MkPaginationType< + typeof pagination.endpoint +> | null>(null); async function create() { const { canceled, result } = await os.form(i18n.ts.createNewClip, { diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue index 990ce45670..5c928ccc28 100644 --- a/packages/client/src/pages/my-lists/index.vue +++ b/packages/client/src/pages/my-lists/index.vue @@ -44,7 +44,9 @@ <script lang="ts" setup> import { computed, ref } from "vue"; -import MkPagination from "@/components/MkPagination.vue"; +import MkPagination, { + type MkPaginationType, +} from "@/components/MkPagination.vue"; import MkButton from "@/components/MkButton.vue"; import MkAvatars from "@/components/MkAvatars.vue"; import MkInfo from "@/components/MkInfo.vue"; @@ -53,7 +55,9 @@ import { i18n } from "@/i18n"; import { definePageMetadata } from "@/scripts/page-metadata"; import icon from "@/scripts/icon"; -const pagingComponent = ref<InstanceType<typeof MkPagination>>(); +const pagingComponent = ref<MkPaginationType< + typeof pagination.endpoint +> | null>(null); const pagination = { endpoint: "users/lists/list" as const, diff --git a/packages/client/src/pages/note-history.vue b/packages/client/src/pages/note-history.vue index fdf8178fbf..e7a8b0d352 100644 --- a/packages/client/src/pages/note-history.vue +++ b/packages/client/src/pages/note-history.vue @@ -34,7 +34,9 @@ <script lang="ts" setup> import { computed, onMounted, ref } from "vue"; -import MkPagination from "@/components/MkPagination.vue"; +import MkPagination, { + type MkPaginationType, +} from "@/components/MkPagination.vue"; import { api } from "@/os"; import XList from "@/components/MkDateSeparatedList.vue"; import XNote from "@/components/MkNote.vue"; @@ -43,7 +45,9 @@ import { definePageMetadata } from "@/scripts/page-metadata"; import icon from "@/scripts/icon"; import type { entities } from "firefish-js"; -const pagingComponent = ref<InstanceType<typeof MkPagination>>(); +const pagingComponent = ref<MkPaginationType< + typeof pagination.endpoint +> | null>(null); const props = defineProps<{ noteId: string; diff --git a/packages/client/src/pages/search.vue b/packages/client/src/pages/search.vue index 972a2d46b2..c7fb067764 100644 --- a/packages/client/src/pages/search.vue +++ b/packages/client/src/pages/search.vue @@ -89,8 +89,9 @@ const usersPagination = { endpoint: "users/search" as const, limit: 10, params: computed(() => ({ - query: props.query, - origin: "combined", + // FIXME: query is necessary for user search + query: props.query!, + origin: "combined" as const, })), }; diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 450a5885c4..873b32d7d7 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -13,16 +13,17 @@ import { getUserMenu } from "@/scripts/get-user-menu"; import icon from "@/scripts/icon"; import { useRouter } from "@/router"; import { notePage } from "@/filters/note"; +import type { NoteTranslation } from "@/types/note"; const router = useRouter(); export function getNoteMenu(props: { note: entities.Note; menuButton: Ref<HTMLElement | undefined>; - translation: Ref<any>; + translation: Ref<NoteTranslation | null>; translating: Ref<boolean>; isDeleted: Ref<boolean>; - currentClipPage?: Ref<entities.Clip>; + currentClipPage?: Ref<entities.Clip> | null; }) { const isRenote = props.note.renote != null && diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts index def3baf8f8..1bc32d5246 100644 --- a/packages/client/src/scripts/use-note-capture.ts +++ b/packages/client/src/scripts/use-note-capture.ts @@ -6,7 +6,7 @@ import { isSignedIn, me } from "@/me"; import * as os from "@/os"; export function useNoteCapture(props: { - rootEl: Ref<HTMLElement>; + rootEl: Ref<HTMLElement | null>; note: Ref<entities.Note>; isDeletedRef: Ref<boolean>; }) { diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index deba55d285..85224f0d86 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -3,11 +3,24 @@ import { isSignedIn } from "./me"; import { Storage } from "./pizzax"; import type { NoteVisibility } from "@/types/note"; -export const postFormActions = []; -export const userActions = []; -export const noteActions = []; -export const noteViewInterruptors = []; -export const notePostInterruptors = []; +export const postFormActions: { + title: string; + handler: (note: entities.Note) => void | Promise<void>; +}[] = []; +export const userActions: { + title: string; + handler: (note: entities.Note) => void | Promise<void>; +}[] = []; +export const noteActions: { + title: string; + handler: (note: entities.Note) => void | Promise<void>; +}[] = []; +export const noteViewInterruptors: { + handler: (note: entities.Note) => Promise<entities.Note>; +}[] = []; +export const notePostInterruptors: { + handler: (note: entities.Note) => Promise<entities.Note>; +}[] = []; const menuOptions = [ "notifications", @@ -453,6 +466,7 @@ import darkTheme from "@/themes/d-rosepine.json5"; * Storage for configuration information that does not need to be constantly loaded into memory (non-reactive) */ import lightTheme from "@/themes/l-rosepinedawn.json5"; +import { entities } from "firefish-js"; export class ColdDeviceStorage { public static default = { diff --git a/packages/client/src/types/note.ts b/packages/client/src/types/note.ts index 559ce0c793..7f4de74d77 100644 --- a/packages/client/src/types/note.ts +++ b/packages/client/src/types/note.ts @@ -1,3 +1,8 @@ import type { noteVisibilities } from "firefish-js"; export type NoteVisibility = (typeof noteVisibilities)[number] | "private"; + +export type NoteTranslation = { + sourceLang: string; + text: string; +}; diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts index 890e6bcd48..ffb22c191b 100644 --- a/packages/firefish-js/src/api.types.ts +++ b/packages/firefish-js/src/api.types.ts @@ -36,6 +36,7 @@ import type { UserDetailed, UserGroup, UserList, + UserLite, UserSorting, } from "./entities"; @@ -686,7 +687,14 @@ export type Endpoints = { res: Note[]; }; "notes/clips": { req: TODO; res: TODO }; - "notes/conversation": { req: TODO; res: TODO }; + "notes/conversation": { + req: { + noteId: string; + limit?: number; + offset?: number; + }; + res: Note[]; + }; "notes/create": { req: NoteSubmitReq; res: { createdNote: Note }; @@ -789,7 +797,24 @@ export type Endpoints = { res: Note[]; }; "notes/search-by-tag": { req: TODO; res: TODO }; - "notes/search": { req: TODO; res: TODO }; + "notes/search": { + req: { + query: string; + sinceId?: string; + untilId?: string; + sinceDate?: number; + untilDate?: number; + limit?: number; + offset?: number; + host?: string; + userId?: string; + withFiles?: boolean; + searchCwAndAlt?: boolean; + channelId?: string; + order?: "chronological" | "relevancy"; + }; + res: Note[]; + }; "notes/show": { req: { noteId: Note["id"] }; res: Note }; "notes/state": { req: TODO; res: TODO }; "notes/timeline": { @@ -802,6 +827,16 @@ export type Endpoints = { }; res: Note[]; }; + "notes/translate": { + req: { + noteId: string; + targetLang: string; + }; + res: { + sourceLang: string; + text: string; + }; + }; "notes/unrenote": { req: { noteId: Note["id"] }; res: null }; "notes/user-list-timeline": { req: { @@ -972,7 +1007,16 @@ export type Endpoints = { "users/relation": { req: TODO; res: TODO }; "users/report-abuse": { req: TODO; res: TODO }; "users/search-by-username-and-host": { req: TODO; res: TODO }; - "users/search": { req: TODO; res: TODO }; + "users/search": { + req: { + query: string; + offset?: number; + limit?: number; + origin?: "local" | "remote" | "combined"; + detail?: true; // FIXME: when false, returns UserLite + }; + res: UserDetailed[]; + }; "users/show": { req: ShowUserReq | { userIds: User["id"][] }; res: { diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index 34c958ea5f..a8a15f2290 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -19,14 +19,7 @@ export type UserLite = { alsoKnownAs: string[]; movedToUri: any; emojis: EmojiLite[]; - instance?: { - name: Instance["name"]; - softwareName: Instance["softwareName"]; - softwareVersion: Instance["softwareVersion"]; - iconUrl: Instance["iconUrl"]; - faviconUrl: Instance["faviconUrl"]; - themeColor: Instance["themeColor"]; - }; + instance?: InstanceLite; }; export type UserDetailed = UserLite & { @@ -556,6 +549,15 @@ export type Blocking = { blockee: UserDetailed; }; +export type InstanceLite = { + name: Instance["name"]; + softwareName: Instance["softwareName"]; + softwareVersion: Instance["softwareVersion"]; + iconUrl: Instance["iconUrl"]; + faviconUrl: Instance["faviconUrl"]; + themeColor: Instance["themeColor"]; +}; + export type Instance = { id: ID; caughtAt: DateString; From 695cb452bc7ddbc66fc8abaa304e54ea1b2f5c31 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 16:50:07 +0800 Subject: [PATCH 035/110] chore: Temporarily disable organizeImports --- biome.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biome.json b/biome.json index 80f0d63eb4..21b711f457 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,7 @@ { "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", "organizeImports": { - "enabled": true + "enabled": false }, "linter": { "enabled": true, From f422842aefc234cf83dc264e3a8939ddad2e7e95 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 16:55:34 +0800 Subject: [PATCH 036/110] use Number.parseInt --- packages/client/src/components/MkChart.vue | 6 +++--- .../client/src/components/MkPollEditor.vue | 2 +- packages/client/src/components/MkWindow.vue | 18 +++++++++--------- .../src/pages/admin/overview.queue-chart.vue | 6 +++--- .../src/pages/admin/queue.chart.chart.vue | 6 +++--- packages/client/src/scripts/2fa.ts | 2 +- packages/client/src/scripts/color.ts | 6 +++--- packages/client/src/scripts/hpml/evaluator.ts | 2 +- packages/client/src/scripts/hpml/lib.ts | 4 ++-- packages/client/src/scripts/physics.ts | 6 +++--- packages/client/src/scripts/popout.ts | 4 ++-- 11 files changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/client/src/components/MkChart.vue b/packages/client/src/components/MkChart.vue index d62f52d26f..abca01f199 100644 --- a/packages/client/src/components/MkChart.vue +++ b/packages/client/src/components/MkChart.vue @@ -100,9 +100,9 @@ const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); const negate = (arr) => arr.map((x) => -x); const alpha = (hex, a) => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; - const r = parseInt(result[1], 16); - const g = parseInt(result[2], 16); - const b = parseInt(result[3], 16); + const r = Number.parseInt(result[1], 16); + const g = Number.parseInt(result[2], 16); + const b = Number.parseInt(result[3], 16); return `rgba(${r}, ${g}, ${b}, ${a})`; }; diff --git a/packages/client/src/components/MkPollEditor.vue b/packages/client/src/components/MkPollEditor.vue index 51bc99ec77..982a432e26 100644 --- a/packages/client/src/components/MkPollEditor.vue +++ b/packages/client/src/components/MkPollEditor.vue @@ -147,7 +147,7 @@ function get() { }; const calcAfter = () => { - let base = parseInt(after.value); + let base = Number.parseInt(after.value); switch (unit.value) { case "day": base *= 24; diff --git a/packages/client/src/components/MkWindow.vue b/packages/client/src/components/MkWindow.vue index 147d0bec1f..8e0747abd5 100644 --- a/packages/client/src/components/MkWindow.vue +++ b/packages/client/src/components/MkWindow.vue @@ -271,7 +271,7 @@ function onHeaderMousedown(evt: MouseEvent) { ? evt.touches[0].clientY : evt.clientY; const moveBaseX = beforeMaximized - ? parseInt(unMaximizedWidth, 10) / 2 + ? Number.parseInt(unMaximizedWidth, 10) / 2 : clickX - position.left; // TODO: parseIntやめる const moveBaseY = beforeMaximized ? 20 : clickY - position.top; const browserWidth = window.innerWidth; @@ -321,8 +321,8 @@ function onTopHandleMousedown(evt) { const main = rootEl.value; const base = evt.clientY; - const height = parseInt(getComputedStyle(main, "").height, 10); - const top = parseInt(getComputedStyle(main, "").top, 10); + const height = Number.parseInt(getComputedStyle(main, "").height, 10); + const top = Number.parseInt(getComputedStyle(main, "").top, 10); // 動かした時 dragListen((me) => { @@ -349,8 +349,8 @@ function onRightHandleMousedown(evt) { const main = rootEl.value; const base = evt.clientX; - const width = parseInt(getComputedStyle(main, "").width, 10); - const left = parseInt(getComputedStyle(main, "").left, 10); + const width = Number.parseInt(getComputedStyle(main, "").width, 10); + const left = Number.parseInt(getComputedStyle(main, "").left, 10); const browserWidth = window.innerWidth; // 動かした時 @@ -375,8 +375,8 @@ function onBottomHandleMousedown(evt) { const main = rootEl.value; const base = evt.clientY; - const height = parseInt(getComputedStyle(main, "").height, 10); - const top = parseInt(getComputedStyle(main, "").top, 10); + const height = Number.parseInt(getComputedStyle(main, "").height, 10); + const top = Number.parseInt(getComputedStyle(main, "").top, 10); const browserHeight = window.innerHeight; // 動かした時 @@ -401,8 +401,8 @@ function onLeftHandleMousedown(evt) { const main = rootEl.value; const base = evt.clientX; - const width = parseInt(getComputedStyle(main, "").width, 10); - const left = parseInt(getComputedStyle(main, "").left, 10); + const width = Number.parseInt(getComputedStyle(main, "").width, 10); + const left = Number.parseInt(getComputedStyle(main, "").left, 10); // 動かした時 dragListen((me) => { diff --git a/packages/client/src/pages/admin/overview.queue-chart.vue b/packages/client/src/pages/admin/overview.queue-chart.vue index cfbcbe6a11..f74cbb7e7e 100644 --- a/packages/client/src/pages/admin/overview.queue-chart.vue +++ b/packages/client/src/pages/admin/overview.queue-chart.vue @@ -47,9 +47,9 @@ const props = defineProps<{ const alpha = (hex, a) => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; - const r = parseInt(result[1], 16); - const g = parseInt(result[2], 16); - const b = parseInt(result[3], 16); + const r = Number.parseInt(result[1], 16); + const g = Number.parseInt(result[2], 16); + const b = Number.parseInt(result[3], 16); return `rgba(${r}, ${g}, ${b}, ${a})`; }; diff --git a/packages/client/src/pages/admin/queue.chart.chart.vue b/packages/client/src/pages/admin/queue.chart.chart.vue index b802457b4a..e8951709f2 100644 --- a/packages/client/src/pages/admin/queue.chart.chart.vue +++ b/packages/client/src/pages/admin/queue.chart.chart.vue @@ -47,9 +47,9 @@ const props = defineProps<{ const alpha = (hex, a) => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; - const r = parseInt(result[1], 16); - const g = parseInt(result[2], 16); - const b = parseInt(result[3], 16); + const r = Number.parseInt(result[1], 16); + const g = Number.parseInt(result[2], 16); + const b = Number.parseInt(result[3], 16); return `rgba(${r}, ${g}, ${b}, ${a})`; }; diff --git a/packages/client/src/scripts/2fa.ts b/packages/client/src/scripts/2fa.ts index 14d59bebec..9c34c8fb70 100644 --- a/packages/client/src/scripts/2fa.ts +++ b/packages/client/src/scripts/2fa.ts @@ -9,7 +9,7 @@ export function byteify(string: string, encoding: "ascii" | "base64" | "hex") { ); case "hex": return new Uint8Array( - string.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)), + string.match(/.{1,2}/g).map((byte) => Number.parseInt(byte, 16)), ); } } diff --git a/packages/client/src/scripts/color.ts b/packages/client/src/scripts/color.ts index 10a99a5a05..10b5ea0e54 100644 --- a/packages/client/src/scripts/color.ts +++ b/packages/client/src/scripts/color.ts @@ -1,7 +1,7 @@ export const alpha = (hex: string, a: number): string => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; - const r = parseInt(result[1], 16); - const g = parseInt(result[2], 16); - const b = parseInt(result[3], 16); + const r = Number.parseInt(result[1], 16); + const g = Number.parseInt(result[2], 16); + const b = Number.parseInt(result[3], 16); return `rgba(${r}, ${g}, ${b}, ${a})`; }; diff --git a/packages/client/src/scripts/hpml/evaluator.ts b/packages/client/src/scripts/hpml/evaluator.ts index ba06a87442..65f8f6a9f5 100644 --- a/packages/client/src/scripts/hpml/evaluator.ts +++ b/packages/client/src/scripts/hpml/evaluator.ts @@ -183,7 +183,7 @@ export class Hpml { } if (expr.type === "number") { - return parseInt(expr.value as any, 10); + return Number.parseInt(expr.value as any, 10); } if (expr.type === "text" || expr.type === "multiLineText") { diff --git a/packages/client/src/scripts/hpml/lib.ts b/packages/client/src/scripts/hpml/lib.ts index 0a2226be8a..06bda34655 100644 --- a/packages/client/src/scripts/hpml/lib.ts +++ b/packages/client/src/scripts/hpml/lib.ts @@ -505,7 +505,7 @@ export function initHpmlLib( strReplace: (a: string, b: string, c: string) => a.split(b).join(c), strReverse: (a: string) => a.split("").reverse().join(""), join: (texts: string[], separator: string) => texts.join(separator || ""), - stringToNumber: (a: string) => parseInt(a), + stringToNumber: (a: string) => Number.parseInt(a), numberToString: (a: number) => a.toString(), splitStrByLine: (a: string) => a.split("\n"), pick: (list: any[], i: number) => list[i - 1], @@ -534,7 +534,7 @@ export function initHpmlLib( let totalFactor = 0; for (const x of list) { const parts = x.split(" "); - const factor = parseInt(parts.pop()!, 10); + const factor = Number.parseInt(parts.pop()!, 10); const text = parts.join(" "); totalFactor += factor; xs.push({ factor, text }); diff --git a/packages/client/src/scripts/physics.ts b/packages/client/src/scripts/physics.ts index 5375c01d29..76dd117a79 100644 --- a/packages/client/src/scripts/physics.ts +++ b/packages/client/src/scripts/physics.ts @@ -65,10 +65,10 @@ export function physics(container: HTMLElement) { const objs = []; for (const objEl of objEls) { const left = objEl.dataset.physicsX - ? parseInt(objEl.dataset.physicsX) + ? Number.parseInt(objEl.dataset.physicsX) : objEl.offsetLeft; const top = objEl.dataset.physicsY - ? parseInt(objEl.dataset.physicsY) + ? Number.parseInt(objEl.dataset.physicsY) : objEl.offsetTop; let obj; @@ -90,7 +90,7 @@ export function physics(container: HTMLElement) { objEl.offsetHeight, { chamfer: { - radius: parseInt(style.borderRadius || "0", 10), + radius: Number.parseInt(style.borderRadius || "0", 10), }, restitution: 0.5, }, diff --git a/packages/client/src/scripts/popout.ts b/packages/client/src/scripts/popout.ts index 54aa422257..9113f4a9a4 100644 --- a/packages/client/src/scripts/popout.ts +++ b/packages/client/src/scripts/popout.ts @@ -9,8 +9,8 @@ export function popout(path: string, w?: HTMLElement) { url = appendQuery(url, "zen"); if (w) { const position = w.getBoundingClientRect(); - const width = parseInt(getComputedStyle(w, "").width, 10); - const height = parseInt(getComputedStyle(w, "").height, 10); + const width = Number.parseInt(getComputedStyle(w, "").width, 10); + const height = Number.parseInt(getComputedStyle(w, "").height, 10); const x = window.screenX + position.left; const y = window.screenY + position.top; window.open( From 393ab2590d770eb590f5e4a8974e155cb9d09fa8 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Fri, 12 Apr 2024 22:02:03 +0800 Subject: [PATCH 037/110] fix type errors of components --- packages/client/src/components/MkButton.vue | 3 +- .../client/src/components/MkChartTooltip.vue | 4 +- packages/client/src/components/MkDialog.vue | 62 +++++++++++-------- packages/client/src/components/MkDrive.vue | 8 +-- .../client/src/components/MkNotification.vue | 54 +++++++++------- .../client/src/components/MkPagePreview.vue | 5 +- .../client/src/components/MkPageWindow.vue | 27 ++++---- .../client/src/components/MkPollEditor.vue | 19 +++--- packages/client/src/components/MkPostForm.vue | 4 +- .../client/src/components/MkReactionIcon.vue | 4 +- .../src/components/MkReactionTooltip.vue | 6 +- .../components/MkReactionsViewer.details.vue | 10 ++- .../components/MkReactionsViewer.reaction.vue | 2 +- .../client/src/components/MkRenoteButton.vue | 2 +- packages/client/src/components/MkTooltip.vue | 28 ++++++--- .../client/src/components/MkUsersTooltip.vue | 8 ++- packages/client/src/components/MkWidgets.vue | 8 +-- packages/client/src/components/global/MkA.vue | 2 +- packages/client/src/os.ts | 10 +-- packages/firefish-js/src/entities.ts | 16 ++++- 20 files changed, 170 insertions(+), 112 deletions(-) diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue index a0ff747afc..ab62fd166c 100644 --- a/packages/client/src/components/MkButton.vue +++ b/packages/client/src/components/MkButton.vue @@ -16,7 +16,7 @@ v-else class="bghgjjyj _button" :class="{ inline, primary, gradate, danger, rounded, full, mini }" - :to="to" + :to="to!" @mousedown="onMousedown" > <div ref="ripples" class="ripples"></div> @@ -36,6 +36,7 @@ const props = defineProps<{ gradate?: boolean; rounded?: boolean; inline?: boolean; + // FIXME: if `link`, `to` is necessary link?: boolean; to?: string; autofocus?: boolean; diff --git a/packages/client/src/components/MkChartTooltip.vue b/packages/client/src/components/MkChartTooltip.vue index 659dc6d399..678a3ccdaa 100644 --- a/packages/client/src/components/MkChartTooltip.vue +++ b/packages/client/src/components/MkChartTooltip.vue @@ -28,11 +28,11 @@ </template> <script lang="ts" setup> -import {} from "vue"; +import type { Ref } from "vue"; import MkTooltip from "./MkTooltip.vue"; const props = defineProps<{ - showing: boolean; + showing: Ref<boolean>; x: number; y: number; title?: string; diff --git a/packages/client/src/components/MkDialog.vue b/packages/client/src/components/MkDialog.vue index 0b9d2cac36..c1f8f581a7 100644 --- a/packages/client/src/components/MkDialog.vue +++ b/packages/client/src/components/MkDialog.vue @@ -104,7 +104,7 @@ </MkInput> <MkTextarea v-if="input && input.type === 'paragraph'" - v-model="inputValue" + v-model="(inputValue as string)" autofocus type="paragraph" :placeholder="input.placeholder || undefined" @@ -204,7 +204,16 @@ import { i18n } from "@/i18n"; import iconify from "@/scripts/icon"; interface Input { - type: HTMLInputElement["type"]; + type?: + | "text" + | "number" + | "password" + | "email" + | "url" + | "date" + | "time" + | "search" + | "paragraph"; placeholder?: string | null; autocomplete?: string; default: string | number | null; @@ -237,8 +246,8 @@ const props = withDefaults( | "question" | "waiting" | "search"; - title: string; - text?: string; + title?: string | null; + text?: string | null; isPlaintext?: boolean; input?: Input; select?: Select; @@ -246,7 +255,7 @@ const props = withDefaults( actions?: { text: string; primary?: boolean; - callback: (...args: any[]) => void; + callback: () => void; }[]; showOkButton?: boolean; showCancelButton?: boolean; @@ -268,7 +277,10 @@ const props = withDefaults( ); const emit = defineEmits<{ - (ev: "done", v: { canceled: boolean; result: any }): void; + ( + ev: "done", + v: { canceled: boolean; result?: string | number | boolean | null }, + ): void; (ev: "closed"): void; }>(); @@ -306,7 +318,7 @@ const okButtonDisabled = computed<boolean>(() => { const inputEl = ref<typeof MkInput>(); -function done(canceled: boolean, result?) { +function done(canceled: boolean, result?: string | number | boolean | null) { emit("done", { canceled, result }); modal.value?.close(null); } @@ -342,12 +354,12 @@ function onInputKeydown(evt: KeyboardEvent) { } } -function formatDateToYYYYMMDD(date) { - const year = date.getFullYear(); - const month = ("0" + (date.getMonth() + 1)).slice(-2); - const day = ("0" + (date.getDate() + 1)).slice(-2); - return `${year}-${month}-${day}`; -} +// function formatDateToYYYYMMDD(date) { +// const year = date.getFullYear(); +// const month = ("0" + (date.getMonth() + 1)).slice(-2); +// const day = ("0" + (date.getDate() + 1)).slice(-2); +// return `${year}-${month}-${day}`; +// } /** * Appends a new search parameter to the value in the input field. @@ -355,18 +367,18 @@ function formatDateToYYYYMMDD(date) { * begin typing a new criteria. * @param value The value to append. */ -function appendFilter(value: string) { - return ( - [ - typeof inputValue.value === "string" - ? inputValue.value.trim() - : inputValue.value, - value, - ] - .join(" ") - .trim() + " " - ); -} +// function appendFilter(value: string) { +// return ( +// [ +// typeof inputValue.value === "string" +// ? inputValue.value.trim() +// : inputValue.value, +// value, +// ] +// .join(" ") +// .trim() + " " +// ); +// } onMounted(() => { document.addEventListener("keydown", onKeydown); diff --git a/packages/client/src/components/MkDrive.vue b/packages/client/src/components/MkDrive.vue index 0273e0b40e..ad2f620f6c 100644 --- a/packages/client/src/components/MkDrive.vue +++ b/packages/client/src/components/MkDrive.vue @@ -253,7 +253,7 @@ function onStreamDriveFolderDeleted(folderId: string) { removeFolder(folderId); } -function onDragover(ev: DragEvent): any { +function onDragover(ev: DragEvent) { if (!ev.dataTransfer) return; // ドラッグ元が自分自身の所有するアイテムだったら @@ -285,7 +285,7 @@ function onDragleave() { draghover.value = false; } -function onDrop(ev: DragEvent): any { +function onDrop(ev: DragEvent) { draghover.value = false; if (!ev.dataTransfer) return; @@ -493,14 +493,12 @@ function move(target?: entities.DriveFolder) { if (!target) { goRoot(); return; - } else if (typeof target === "object") { - target = target.id; } fetching.value = true; os.api("drive/folders/show", { - folderId: target, + folderId: target.id, }).then((folderToMove) => { folder.value = folderToMove; hierarchyFolders.value = []; diff --git a/packages/client/src/components/MkNotification.vue b/packages/client/src/components/MkNotification.vue index b00646cdd6..6dd1a4b721 100644 --- a/packages/client/src/components/MkNotification.vue +++ b/packages/client/src/components/MkNotification.vue @@ -12,12 +12,12 @@ :user="notification.note.user" /> <MkAvatar - v-else-if="notification.user" + v-else-if="'user' in notification" class="icon" :user="notification.user" /> <img - v-else-if="notification.icon" + v-else-if="'icon' in notification && notification.icon" class="icon" :src="notification.icon" alt="" @@ -95,7 +95,7 @@ i18n.ts._notification.pollEnded }}</span> <MkA - v-else-if="notification.user" + v-else-if="'user' in notification" v-user-preview="notification.user.id" class="name" :to="userPage(notification.user)" @@ -133,7 +133,7 @@ :plain="true" :nowrap="!full" :lang="notification.note.lang" - :custom-emojis="notification.note.renote.emojis" + :custom-emojis="notification.note.renote!.emojis" /> </MkA> <MkA @@ -212,6 +212,7 @@ style="opacity: 0.7" >{{ i18n.ts.youGotNewFollower }} <div v-if="full && !hideFollowButton"> + <!-- FIXME: Provide a UserDetailed here --> <MkFollowButton :user="notification.user" :full="true" @@ -269,7 +270,7 @@ </template> <script lang="ts" setup> -import { onMounted, onUnmounted, ref, watch } from "vue"; +import { onMounted, onUnmounted, ref, toRef, watch } from "vue"; import type { entities } from "firefish-js"; import XReactionIcon from "@/components/MkReactionIcon.vue"; import MkFollowButton from "@/components/MkFollowButton.vue"; @@ -284,6 +285,8 @@ import { useTooltip } from "@/scripts/use-tooltip"; import { defaultStore } from "@/store"; import { instance } from "@/instance"; import icon from "@/scripts/icon"; +import type { Connection } from "firefish-js/src/streaming"; +import type { Channels } from "firefish-js/src/streaming.types"; const props = withDefaults( defineProps<{ @@ -299,8 +302,8 @@ const props = withDefaults( const stream = useStream(); -const elRef = ref<HTMLElement>(null); -const reactionRef = ref(null); +const elRef = ref<HTMLElement | null>(null); +const reactionRef = ref<InstanceType<typeof XReactionIcon> | null>(null); const hideFollowButton = defaultStore.state.hideFollowButtons; const showEmojiReactions = @@ -311,7 +314,7 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact : "⭐"; let readObserver: IntersectionObserver | undefined; -let connection; +let connection: Connection<Channels["main"]> | null = null; onMounted(() => { if (!props.notification.isRead) { @@ -323,13 +326,13 @@ onMounted(() => { observer.disconnect(); }); - readObserver.observe(elRef.value); + readObserver.observe(elRef.value!); connection = stream.useChannel("main"); - connection.on("readAllNotifications", () => readObserver.disconnect()); + connection.on("readAllNotifications", () => readObserver!.disconnect()); - watch(props.notification.isRead, () => { - readObserver.disconnect(); + watch(toRef(props.notification.isRead), () => { + readObserver!.disconnect(); }); } }); @@ -344,38 +347,47 @@ const groupInviteDone = ref(false); const acceptFollowRequest = () => { followRequestDone.value = true; - os.api("following/requests/accept", { userId: props.notification.user.id }); + os.api("following/requests/accept", { + userId: (props.notification as entities.ReceiveFollowRequestNotification) + .user.id, + }); }; const rejectFollowRequest = () => { followRequestDone.value = true; - os.api("following/requests/reject", { userId: props.notification.user.id }); + os.api("following/requests/reject", { + userId: (props.notification as entities.ReceiveFollowRequestNotification) + .user.id, + }); }; const acceptGroupInvitation = () => { groupInviteDone.value = true; os.apiWithDialog("users/groups/invitations/accept", { - invitationId: props.notification.invitation.id, + invitationId: (props.notification as entities.GroupInvitedNotification) + .invitation.id, }); }; const rejectGroupInvitation = () => { groupInviteDone.value = true; os.api("users/groups/invitations/reject", { - invitationId: props.notification.invitation.id, + invitationId: (props.notification as entities.GroupInvitedNotification) + .invitation.id, }); }; useTooltip(reactionRef, (showing) => { + const n = props.notification as entities.ReactionNotification; os.popup( XReactionTooltip, { showing, - reaction: props.notification.reaction - ? props.notification.reaction.replace(/^:(\w+):$/, ":$1@.:") - : props.notification.reaction, - emojis: props.notification.note.emojis, - targetElement: reactionRef.value.$el, + reaction: n.reaction + ? n.reaction.replace(/^:(\w+):$/, ":$1@.:") + : n.reaction, + emojis: n.note.emojis, + targetElement: reactionRef.value!.$el, }, {}, "closed", diff --git a/packages/client/src/components/MkPagePreview.vue b/packages/client/src/components/MkPagePreview.vue index 034c6fed63..3a6a45745d 100644 --- a/packages/client/src/components/MkPagePreview.vue +++ b/packages/client/src/components/MkPagePreview.vue @@ -3,7 +3,7 @@ :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _block" tabindex="-1" - :behavior="`${ui === 'deck' ? 'window' : null}`" + :behavior="ui === 'deck' ? 'window' : null" > <div v-if="page.eyeCatchingImage" @@ -36,9 +36,10 @@ <script lang="ts" setup> import { userName } from "@/filters/user"; import { ui } from "@/config"; +import type { entities } from "firefish-js"; defineProps<{ - page: any; + page: entities.Page; }>(); </script> diff --git a/packages/client/src/components/MkPageWindow.vue b/packages/client/src/components/MkPageWindow.vue index d237f18091..082f9f0159 100644 --- a/packages/client/src/components/MkPageWindow.vue +++ b/packages/client/src/components/MkPageWindow.vue @@ -56,23 +56,22 @@ const router = new Router(routes, props.initialPath); const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); const windowEl = ref<InstanceType<typeof XWindow>>(); -const history = ref<{ path: string; key: any }[]>([ +const history = ref<{ path: string; key: string }[]>([ { path: router.getCurrentPath(), key: router.getCurrentKey(), }, ]); const buttonsLeft = computed(() => { - const buttons = []; - if (history.value.length > 1) { - buttons.push({ - icon: `${icon("ph-caret-left")}`, - onClick: back, - }); + return [ + { + icon: `${icon("ph-caret-left")}`, + onClick: back, + }, + ]; } - - return buttons; + return []; }); const buttonsRight = computed(() => { const buttons = [ @@ -114,7 +113,7 @@ const contextmenu = computed(() => [ text: i18n.ts.openInNewTab, action: () => { window.open(url + router.getCurrentPath(), "_blank"); - windowEl.value.close(); + windowEl.value!.close(); }, }, { @@ -135,17 +134,17 @@ function back() { } function close() { - windowEl.value.close(); + windowEl.value!.close(); } function expand() { mainRouter.push(router.getCurrentPath(), "forcePage"); - windowEl.value.close(); + windowEl.value!.close(); } function popout() { - _popout(router.getCurrentPath(), windowEl.value.$el); - windowEl.value.close(); + _popout(router.getCurrentPath(), windowEl.value!.$el); + windowEl.value!.close(); } defineExpose({ diff --git a/packages/client/src/components/MkPollEditor.vue b/packages/client/src/components/MkPollEditor.vue index 982a432e26..b05f80fafa 100644 --- a/packages/client/src/components/MkPollEditor.vue +++ b/packages/client/src/components/MkPollEditor.vue @@ -94,15 +94,14 @@ const props = defineProps<{ }; }>(); const emit = defineEmits<{ - ( - ev: "update:modelValue", + "update:modelValue": [ v: { - expiresAt: string; - expiredAfter: number; + expiresAt?: number; + expiredAfter?: number | null; choices: string[]; multiple: boolean; }, - ): void; + ]; }>(); const choices = ref(props.modelValue.choices); @@ -147,19 +146,19 @@ function get() { }; const calcAfter = () => { - let base = Number.parseInt(after.value); + let base = Number.parseInt(after.value.toString()); switch (unit.value) { + // biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially case "day": base *= 24; - // fallthrough + // biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially case "hour": base *= 60; - // fallthrough + // biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially case "minute": base *= 60; - // fallthrough case "second": - return (base *= 1000); + return base * 1000; default: return null; } diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue index fb4a6dc740..2735eb55a3 100644 --- a/packages/client/src/components/MkPostForm.vue +++ b/packages/client/src/components/MkPostForm.vue @@ -1136,11 +1136,11 @@ async function post() { nextTick(() => autosize.update(textareaEl.value)); }); }) - .catch((err) => { + .catch((err: { message: string; id: string }) => { posting.value = false; os.alert({ type: "error", - text: err.message + "\n" + (err as any).id, + text: `${err.message}\n${err.id}`, }); }); vibrate([10, 20, 10, 20, 10, 20, 60]); diff --git a/packages/client/src/components/MkReactionIcon.vue b/packages/client/src/components/MkReactionIcon.vue index e9d5a198cc..6608501478 100644 --- a/packages/client/src/components/MkReactionIcon.vue +++ b/packages/client/src/components/MkReactionIcon.vue @@ -9,9 +9,11 @@ </template> <script lang="ts" setup> +import type { entities } from "firefish-js"; + defineProps<{ reaction: string; - customEmojis?: any[]; // TODO + customEmojis?: entities.EmojiLite[]; noStyle?: boolean; }>(); </script> diff --git a/packages/client/src/components/MkReactionTooltip.vue b/packages/client/src/components/MkReactionTooltip.vue index 0e83226c94..1286fc4c73 100644 --- a/packages/client/src/components/MkReactionTooltip.vue +++ b/packages/client/src/components/MkReactionTooltip.vue @@ -3,6 +3,7 @@ ref="tooltip" :target-element="targetElement" :max-width="340" + :showing="showing" @closed="emit('closed')" > <div class="beeadbfb"> @@ -18,12 +19,15 @@ </template> <script lang="ts" setup> +import type { Ref } from "vue"; import MkTooltip from "./MkTooltip.vue"; import XReactionIcon from "@/components/MkReactionIcon.vue"; +import type { entities } from "firefish-js"; defineProps<{ + showing: Ref<boolean>; reaction: string; - emojis: any[]; // TODO + emojis: entities.EmojiLite[]; targetElement: HTMLElement; }>(); diff --git a/packages/client/src/components/MkReactionsViewer.details.vue b/packages/client/src/components/MkReactionsViewer.details.vue index 0d992ae431..14c2828d45 100644 --- a/packages/client/src/components/MkReactionsViewer.details.vue +++ b/packages/client/src/components/MkReactionsViewer.details.vue @@ -4,6 +4,7 @@ :target-element="targetElement" :max-width="340" @closed="emit('closed')" + :showing="showing" > <div class="bqxuuuey"> <div class="reaction"> @@ -29,15 +30,18 @@ </template> <script lang="ts" setup> +import type { Ref } from "vue"; import MkTooltip from "./MkTooltip.vue"; import XReactionIcon from "@/components/MkReactionIcon.vue"; +import type { entities } from "firefish-js"; defineProps<{ + showing: Ref<boolean>; reaction: string; - users: any[]; // TODO + users: entities.User[]; // TODO count: number; - emojis: any[]; // TODO - targetElement: HTMLElement; + emojis: entities.EmojiLite[]; // TODO + targetElement?: HTMLElement; }>(); const emit = defineEmits<{ diff --git a/packages/client/src/components/MkReactionsViewer.reaction.vue b/packages/client/src/components/MkReactionsViewer.reaction.vue index c403c7003c..89f51797ab 100644 --- a/packages/client/src/components/MkReactionsViewer.reaction.vue +++ b/packages/client/src/components/MkReactionsViewer.reaction.vue @@ -89,7 +89,7 @@ useTooltip( emojis: props.note.emojis, users, count: props.count, - targetElement: buttonRef.value, + targetElement: buttonRef.value!, }, {}, "closed", diff --git a/packages/client/src/components/MkRenoteButton.vue b/packages/client/src/components/MkRenoteButton.vue index 845298b89d..7250757da4 100644 --- a/packages/client/src/components/MkRenoteButton.vue +++ b/packages/client/src/components/MkRenoteButton.vue @@ -46,7 +46,7 @@ const buttonRef = ref<HTMLElement>(); const canRenote = computed( () => ["public", "home"].includes(props.note.visibility) || - props.note.userId === me.id, + props.note.userId === me?.id, ); useTooltip(buttonRef, async (showing) => { diff --git a/packages/client/src/components/MkTooltip.vue b/packages/client/src/components/MkTooltip.vue index 883067c51f..2ed3c1974b 100644 --- a/packages/client/src/components/MkTooltip.vue +++ b/packages/client/src/components/MkTooltip.vue @@ -5,13 +5,13 @@ @after-leave="emit('closed')" > <div - v-show="showing" + v-show="unref(showing)" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }" > <slot> - <Mfm v-if="asMfm" :text="text" /> + <Mfm v-if="asMfm" :text="text!" /> <span v-else>{{ text }}</span> </slot> </div> @@ -19,15 +19,22 @@ </template> <script lang="ts" setup> -import { nextTick, onMounted, onUnmounted, ref } from "vue"; +import { + type MaybeRef, + nextTick, + onMounted, + onUnmounted, + ref, + unref, +} from "vue"; import * as os from "@/os"; import { calcPopupPosition } from "@/scripts/popup-position"; import { defaultStore } from "@/store"; const props = withDefaults( defineProps<{ - showing: boolean; - targetElement?: HTMLElement; + showing: MaybeRef<boolean>; + targetElement?: HTMLElement | null; x?: number; y?: number; text?: string; @@ -40,6 +47,7 @@ const props = withDefaults( maxWidth: 250, direction: "top", innerMargin: 0, + targetElement: null, }, ); @@ -51,7 +59,7 @@ const el = ref<HTMLElement>(); const zIndex = os.claimZIndex("high"); function setPosition() { - const data = calcPopupPosition(el.value, { + const data = calcPopupPosition(el.value!, { anchorElement: props.targetElement, direction: props.direction, align: "center", @@ -60,12 +68,12 @@ function setPosition() { y: props.y, }); - el.value.style.transformOrigin = data.transformOrigin; - el.value.style.left = data.left + "px"; - el.value.style.top = data.top + "px"; + el.value!.style.transformOrigin = data.transformOrigin; + el.value!.style.left = `${data.left}px`; + el.value!.style.top = `${data.top}px`; } -let loopHandler; +let loopHandler: number; onMounted(() => { nextTick(() => { diff --git a/packages/client/src/components/MkUsersTooltip.vue b/packages/client/src/components/MkUsersTooltip.vue index 25af3ac121..741194221d 100644 --- a/packages/client/src/components/MkUsersTooltip.vue +++ b/packages/client/src/components/MkUsersTooltip.vue @@ -4,6 +4,7 @@ :target-element="targetElement" :max-width="250" @closed="emit('closed')" + :showing="showing" > <div class="beaffaef"> <div v-for="u in users" :key="u.id" class="user"> @@ -18,12 +19,15 @@ </template> <script lang="ts" setup> +import type { Ref } from "vue"; import MkTooltip from "./MkTooltip.vue"; +import type { entities } from "firefish-js"; defineProps<{ - users: any[]; // TODO + showing: Ref<boolean>; + users: entities.User[]; count: number; - targetElement: HTMLElement; + targetElement?: HTMLElement; }>(); const emit = defineEmits<{ diff --git a/packages/client/src/components/MkWidgets.vue b/packages/client/src/components/MkWidgets.vue index 1b1dafa522..fb8a449590 100644 --- a/packages/client/src/components/MkWidgets.vue +++ b/packages/client/src/components/MkWidgets.vue @@ -85,7 +85,7 @@ import icon from "@/scripts/icon"; interface Widget { name: string; id: string; - data: Record<string, any>; + data: Record<string, unknown>; } const props = defineProps<{ @@ -137,12 +137,12 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { return isLink(el.parentElement); } }; - if (isLink(ev.target)) return; + if (isLink(ev.target as HTMLElement)) return; if ( ["INPUT", "TEXTAREA", "IMG", "VIDEO", "CANVAS"].includes( - ev.target.tagName, + (ev.target as HTMLElement).tagName, ) || - ev.target.attributes.contenteditable + (ev.target as HTMLElement).getAttribute("contentEditable") ) return; if (window.getSelection()?.toString() !== "") return; diff --git a/packages/client/src/components/global/MkA.vue b/packages/client/src/components/global/MkA.vue index b85774dced..fbe5472a24 100644 --- a/packages/client/src/components/global/MkA.vue +++ b/packages/client/src/components/global/MkA.vue @@ -22,7 +22,7 @@ import icon from "@/scripts/icon"; const props = withDefaults( defineProps<{ - to?: string; + to: string; activeClass?: null | string; behavior?: null | "window" | "browser" | "modalWindow"; }>(), diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 04c658ec89..ac1227b827 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -210,11 +210,13 @@ interface VueComponentConstructor<P, E> { emits?: E; } +type NonArrayAble<A> = A extends Array<unknown> ? never : A; + export async function popup<Props, Emits>( component: VueComponentConstructor<Props, Emits>, - props: Props & Record<string, unknown>, - events: Partial<Emits> = {}, - disposeEvent?: string, + props: Props, + events: Partial<NonArrayAble<NonNullable<Emits>>> = {}, + disposeEvent?: keyof Partial<NonArrayAble<NonNullable<Emits>>>, ) { markRaw(component); @@ -227,7 +229,7 @@ export async function popup<Props, Emits>( }; const state = { component, - props, + props: props as Record<string, unknown>, events: disposeEvent ? { ...events, diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index a8a15f2290..5858dacb43 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -20,6 +20,16 @@ export type UserLite = { movedToUri: any; emojis: EmojiLite[]; instance?: InstanceLite; + avatarColor: null; + emojiModPerm: "unauthorized" | "add" | "mod" | "full"; + isAdmin?: boolean; + isModerator?: boolean; + isBot?: boolean; + isLocked: boolean; + isIndexable: boolean; + isCat?: boolean; + speakAsCat?: boolean; + driveCapacityOverrideMb: number | null, }; export type UserDetailed = UserLite & { @@ -46,7 +56,6 @@ export type UserDetailed = UserLite & { isCat: boolean; isFollowed: boolean; isFollowing: boolean; - isLocked: boolean; isModerator: boolean; isMuted: boolean; isRenoteMuted: boolean; @@ -228,7 +237,10 @@ export interface RenoteNotification extends BaseNotification { type: "renote"; user: User; userId: User["id"]; - note: Note; + note: Note & { + renote: Note, + renoteId: string, + }; } export interface QuoteNotification extends BaseNotification { type: "quote"; From 68be7d6be9f16329f2fe64a9ea4bbaf75c1f117f Mon Sep 17 00:00:00 2001 From: jolupa <jolupameister@gmail.com> Date: Fri, 12 Apr 2024 06:38:32 +0000 Subject: [PATCH 038/110] locale: update translations (Catalan) Currently translated at 100.0% (1920 of 1920 strings) Translation: Firefish/locales Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ca/ --- locales/ca-ES.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index b8049c1b18..78ddcc2f55 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -2084,9 +2084,9 @@ _experiments: release: Publicà title: Experiments enablePostImports: Activar l'importació de publicacions - postImportsCaption: Permet els usuaris importar publicacions desde comptes a Firefish, + postImportsCaption: Permet als usuaris importar publicacions des de comptes de Firefish, Misskey, Mastodon, Akkoma i Pleroma. Pot fer que el servidor vagi més lent durant - la càrrega si tens un coll d'ampolla a la cua. + la importació si la teva cua de feina és saturada. noGraze: Si us plau, desactiva l'extensió del navegador "Graze for Mastodon", ja que interfereix amb Firefish. accessibility: Accessibilitat From 2b88ef18a5e90c1a51e5de98b42759d96ece28d7 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Sat, 13 Apr 2024 15:37:23 +0800 Subject: [PATCH 039/110] fix type errors of components --- packages/client/src/components/MkNoteSub.vue | 2 +- packages/client/src/components/MkSignin.vue | 10 +++---- packages/client/src/components/MkSignup.vue | 27 ++++++++++--------- .../src/components/MkUserSelectDialog.vue | 14 +++++----- .../src/components/MkVisibilityPicker.vue | 10 +++---- 5 files changed, 32 insertions(+), 31 deletions(-) diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue index ba91221b87..08c0bf6c38 100644 --- a/packages/client/src/components/MkNoteSub.vue +++ b/packages/client/src/components/MkNoteSub.vue @@ -414,7 +414,7 @@ function onContextmenu(ev: MouseEvent): void { os.pageWindow(notePage(appearNote.value)); }, }, - notePage(appearNote.value) != location.pathname + notePage(appearNote.value) !== location.pathname ? { icon: `${icon("ph-arrows-out-simple")}`, text: i18n.ts.showInPage, diff --git a/packages/client/src/components/MkSignin.vue b/packages/client/src/components/MkSignin.vue index f5c5f0ee4d..a49662d974 100644 --- a/packages/client/src/components/MkSignin.vue +++ b/packages/client/src/components/MkSignin.vue @@ -145,9 +145,10 @@ import * as os from "@/os"; import { signIn } from "@/account"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; +import type { entities } from "firefish-js"; const signing = ref(false); -const user = ref(null); +const user = ref<entities.UserDetailed | null>(null); const username = ref(""); const password = ref(""); const token = ref(""); @@ -249,7 +250,7 @@ function queryKey() { function onSubmit() { signing.value = true; console.log("submit"); - if (window.PublicKeyCredential && user.value.securityKeys) { + if (window.PublicKeyCredential && user.value?.securityKeys) { os.api("signin", { username: username.value, password: password.value, @@ -263,7 +264,7 @@ function onSubmit() { return queryKey(); }) .catch(loginFailed); - } else if (!totpLogin.value && user.value && user.value.twoFactorEnabled) { + } else if (!totpLogin.value && user.value?.twoFactorEnabled) { totpLogin.value = true; signing.value = false; } else { @@ -272,8 +273,7 @@ function onSubmit() { password: password.value, "hcaptcha-response": hCaptchaResponse.value, "g-recaptcha-response": reCaptchaResponse.value, - token: - user.value && user.value.twoFactorEnabled ? token.value : undefined, + token: user.value?.twoFactorEnabled ? token.value : undefined, }) .then((res) => { emit("login", res); diff --git a/packages/client/src/components/MkSignup.vue b/packages/client/src/components/MkSignup.vue index 6de17b2d48..9870263b7a 100644 --- a/packages/client/src/components/MkSignup.vue +++ b/packages/client/src/components/MkSignup.vue @@ -305,12 +305,12 @@ const host = toUnicode(config.host); const hcaptcha = ref(); const recaptcha = ref(); -const username: string = ref(""); -const password: string = ref(""); -const retypedPassword: string = ref(""); -const invitationCode: string = ref(""); +const username = ref<string>(""); +const password = ref<string>(""); +const retypedPassword = ref<string>(""); +const invitationCode = ref<string>(""); const email = ref(""); -const usernameState: +const usernameState = ref< | null | "wait" | "ok" @@ -318,9 +318,10 @@ const usernameState: | "error" | "invalid-format" | "min-range" - | "max-range" = ref(null); -const invitationState: null | "entered" = ref(null); -const emailState: + | "max-range" + >(null); +const invitationState = ref<null | "entered">(null); +const emailState = ref< | null | "wait" | "ok" @@ -330,11 +331,11 @@ const emailState: | "unavailable:mx" | "unavailable:smtp" | "unavailable" - | "error" = ref(null); -const passwordStrength: "" | "low" | "medium" | "high" = ref(""); -const passwordRetypeState: null | "match" | "not-match" = ref(null); -const submitting: boolean = ref(false); -const ToSAgreement: boolean = ref(false); + | "error">(null); +const passwordStrength = ref<"" | "low" | "medium" | "high">(""); +const passwordRetypeState = ref<null | "match" | "not-match" >(null); +const submitting = ref(false); +const ToSAgreement = ref(false); const hCaptchaResponse = ref(null); const reCaptchaResponse = ref(null); diff --git a/packages/client/src/components/MkUserSelectDialog.vue b/packages/client/src/components/MkUserSelectDialog.vue index 3017e9bd6c..aa3a4c63c1 100644 --- a/packages/client/src/components/MkUserSelectDialog.vue +++ b/packages/client/src/components/MkUserSelectDialog.vue @@ -98,16 +98,16 @@ import { defaultStore } from "@/store"; import { i18n } from "@/i18n"; const emit = defineEmits<{ - (ev: "ok", selected: entities.UserDetailed): void; - (ev: "cancel"): void; - (ev: "closed"): void; + ok: [selected: entities.UserDetailed]; + cancel: []; + closed: []; }>(); const username = ref(""); const host = ref(""); -const users: entities.UserDetailed[] = ref([]); -const recentUsers: entities.UserDetailed[] = ref([]); -const selected: entities.UserDetailed | null = ref(null); +const users = ref<entities.UserDetailed[]>([]); +const recentUsers = ref<entities.UserDetailed[]>([]); +const selected = ref<entities.UserDetailed | null>(null); const dialogEl = ref(); const search = () => { @@ -132,7 +132,7 @@ const ok = () => { // 最近使ったユーザー更新 let recents = defaultStore.state.recentlyUsedUsers; - recents = recents.filter((x) => x !== selected.value.id); + recents = recents.filter((x) => x !== selected.value!.id); recents.unshift(selected.value.id); defaultStore.set("recentlyUsedUsers", recents.splice(0, 16)); }; diff --git a/packages/client/src/components/MkVisibilityPicker.vue b/packages/client/src/components/MkVisibilityPicker.vue index 0e6f8cb153..088da07eb0 100644 --- a/packages/client/src/components/MkVisibilityPicker.vue +++ b/packages/client/src/components/MkVisibilityPicker.vue @@ -3,7 +3,7 @@ ref="modal" :z-priority="'high'" :src="src" - @click="modal.close()" + @click="modal!.close()" @closed="emit('closed')" > <div class="_popup" :class="$style.root"> @@ -159,9 +159,9 @@ const props = withDefaults( ); const emit = defineEmits<{ - (ev: "changeVisibility", v: NoteVisibility): void; - (ev: "changeLocalOnly", v: boolean): void; - (ev: "closed"): void; + changeVisibility: [v: NoteVisibility]; + changeLocalOnly: [v: boolean]; + closed: []; }>(); const v = ref(props.currentVisibility); @@ -175,7 +175,7 @@ function choose(visibility: NoteVisibility): void { v.value = visibility; emit("changeVisibility", visibility); nextTick(() => { - modal.value.close(); + modal.value!.close(); }); } </script> From baba86a2030d306fb382d355f17d8b410c62afd5 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Sat, 13 Apr 2024 17:55:40 +0800 Subject: [PATCH 040/110] Downgrade vue-tsc new versions of vue-tsc have perfomance issues: see https://github.com/vuejs/language-tools/issues/4223 --- packages/client/package.json | 2 +- pnpm-lock.yaml | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/client/package.json b/packages/client/package.json index 594f07c607..154d8f33cc 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -88,6 +88,6 @@ "vue-draggable-plus": "^0.4.0", "vue-plyr": "^7.0.0", "vue-prism-editor": "2.0.0-alpha.2", - "vue-tsc": "2.0.12" + "vue-tsc": "2.0.7" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 192b5f95e5..d39e984e81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -783,8 +783,8 @@ importers: specifier: 2.0.0-alpha.2 version: 2.0.0-alpha.2(vue@3.4.21) vue-tsc: - specifier: 2.0.12 - version: 2.0.12(typescript@5.4.5) + specifier: 2.0.7 + version: 2.0.7(typescript@5.4.5) packages/firefish-js: dependencies: @@ -4833,22 +4833,22 @@ packages: vue: 3.4.21(typescript@5.4.5) dev: true - /@volar/language-core@2.2.0-alpha.7: - resolution: {integrity: sha512-igpp+nTkyl8faVzRJMpSCeA4XlBJ5UVSyc/WGyksmUmP10YbfufbcQCFlxEXv2uMBV+a3L4JVCj+Vju+08FOSA==} + /@volar/language-core@2.1.6: + resolution: {integrity: sha512-pAlMCGX/HatBSiDFMdMyqUshkbwWbLxpN/RL7HCQDOo2gYBE+uS+nanosLc1qR6pTQ/U8q00xt8bdrrAFPSC0A==} dependencies: - '@volar/source-map': 2.2.0-alpha.7 + '@volar/source-map': 2.1.6 dev: true - /@volar/source-map@2.2.0-alpha.7: - resolution: {integrity: sha512-iIZM2EovdEnr6mMwlsnt4ciix4xz7HSGHyUSviRaY5cii5PMXGHeUU9UDeb+xzLCx8kdk3L5J4z+ts50AhkYcg==} + /@volar/source-map@2.1.6: + resolution: {integrity: sha512-TeyH8pHHonRCHYI91J7fWUoxi0zWV8whZTVRlsWHSYfjm58Blalkf9LrZ+pj6OiverPTmrHRkBsG17ScQyWECw==} dependencies: muggle-string: 0.4.1 dev: true - /@volar/typescript@2.2.0-alpha.7: - resolution: {integrity: sha512-qy04/hx4UbW1BdPlzaxlH60D4plubcyqdbYM6Y5vZiascZxFowtd6vE39Td9FYzDxwcKgzb/Crvf/ABhdHnuBA==} + /@volar/typescript@2.1.6: + resolution: {integrity: sha512-JgPGhORHqXuyC3r6skPmPHIZj4LoMmGlYErFTuPNBq9Nhc9VTv7ctHY7A3jMN3ngKEfRrfnUcwXHztvdSQqNfw==} dependencies: - '@volar/language-core': 2.2.0-alpha.7 + '@volar/language-core': 2.1.6 path-browserify: 1.0.1 dev: true @@ -4898,15 +4898,15 @@ packages: '@vue/shared': 3.4.21 dev: true - /@vue/language-core@2.0.12(typescript@5.4.5): - resolution: {integrity: sha512-aIStDPt69SHOpiIckGTIIjEz/sXc6ZfCMS5uWYL1AcbcRMhzFCLZscGAVte1+ad+RRFepSpKBjGttyPcgKJ7ww==} + /@vue/language-core@2.0.7(typescript@5.4.5): + resolution: {integrity: sha512-Vh1yZX3XmYjn9yYLkjU8DN6L0ceBtEcapqiyclHne8guG84IaTzqtvizZB1Yfxm3h6m7EIvjerLO5fvOZO6IIQ==} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@volar/language-core': 2.2.0-alpha.7 + '@volar/language-core': 2.1.6 '@vue/compiler-dom': 3.4.21 '@vue/shared': 3.4.21 computeds: 0.0.1 @@ -17161,14 +17161,14 @@ packages: he: 1.2.0 dev: true - /vue-tsc@2.0.12(typescript@5.4.5): - resolution: {integrity: sha512-thlBBWlPYrNdba535oDdxz7PRUufZgRZRVP5Aql5wBVpGSWSeqou4EzFXeKVoZr59lp9hJROubDVzlhACmcEhg==} + /vue-tsc@2.0.7(typescript@5.4.5): + resolution: {integrity: sha512-LYa0nInkfcDBB7y8jQ9FQ4riJTRNTdh98zK/hzt4gEpBZQmf30dPhP+odzCa+cedGz6B/guvJEd0BavZaRptjg==} hasBin: true peerDependencies: typescript: '*' dependencies: - '@volar/typescript': 2.2.0-alpha.7 - '@vue/language-core': 2.0.12(typescript@5.4.5) + '@volar/typescript': 2.1.6 + '@vue/language-core': 2.0.7(typescript@5.4.5) semver: 7.6.0 typescript: 5.4.5 dev: true From d0de0d14b29f8dc96090520fe448c50d62d2e301 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sat, 13 Apr 2024 19:41:30 +0900 Subject: [PATCH 041/110] docs: fix indent --- docs/notice-for-admins.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/notice-for-admins.md b/docs/notice-for-admins.md index abfaaea2fc..c22b1da0aa 100644 --- a/docs/notice-for-admins.md +++ b/docs/notice-for-admins.md @@ -21,8 +21,8 @@ The number of posts stored on your database can be found at `https://yourserver. - Please remove `packages/backend-rs/target` before building Firefish. ```sh - rm --recursive --force packages/backend-rs/target - ``` + rm --recursive --force packages/backend-rs/target + ``` - Please do not terminate `pnpm run migrate` even if it appears to be frozen. ### For Docker/Podman users From d57c9dc2890d3e3c3e30e7292d3f486cb2631639 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sat, 13 Apr 2024 19:46:31 +0900 Subject: [PATCH 042/110] fix (client): set displayMode to true for block math expressions --- packages/client/src/components/MkFormulaCore.vue | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/client/src/components/MkFormulaCore.vue b/packages/client/src/components/MkFormulaCore.vue index 2db4c7d00d..507740d106 100644 --- a/packages/client/src/components/MkFormulaCore.vue +++ b/packages/client/src/components/MkFormulaCore.vue @@ -20,12 +20,10 @@ export default defineComponent({ }, computed: { compiledFormula(): any { - const katexString = katex.renderToString(this.formula, { + return katex.renderToString(this.formula, { throwOnError: false, + displayMode: this.block, } as any); - return this.block - ? `<div style="text-align:center">${katexString}</div>` - : katexString; }, }, }); From 799ad1f3f8fe9a7bda99144415fdade3c7d934be Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sat, 13 Apr 2024 19:49:31 +0900 Subject: [PATCH 043/110] dev (minor, backend-rs): format Makefile --- packages/backend-rs/Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend-rs/Makefile b/packages/backend-rs/Makefile index f91a56aae7..eb4b30c6df 100644 --- a/packages/backend-rs/Makefile +++ b/packages/backend-rs/Makefile @@ -7,9 +7,9 @@ SRC += $(call recursive_wildcard, src, *) .PHONY: regenerate-entities regenerate-entities: sea-orm-cli generate entity \ - --output-dir='src/model/entity' \ - --database-url='postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:25432/$(POSTGRES_DB)' \ - --date-time-crate='chrono' \ + --output-dir='src/model/entity' \ + --database-url='postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:25432/$(POSTGRES_DB)' \ + --date-time-crate='chrono' \ --model-extra-attributes='NAPI_EXTRA_ATTR_PLACEHOLDER' && \ for file in src/model/entity/*; do \ base=$$(basename -- "$${file}"); \ From ad58ae8f3077136cbead48fdbd9024d4d7ab7cbc Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sat, 13 Apr 2024 20:03:02 +0900 Subject: [PATCH 044/110] refactor: remove /api/patrons endpoint --- docs/api-change.md | 4 + locales/ar-SA.yml | 3 - locales/bn-BD.yml | 2 - locales/ca-ES.yml | 6 - locales/de-DE.yml | 6 - locales/en-US.yml | 6 - locales/es-ES.yml | 6 - locales/fr-FR.yml | 6 - locales/id-ID.yml | 6 - locales/it-IT.yml | 6 - locales/ja-JP.yml | 4 - locales/ja-KS.yml | 2 - locales/ko-KR.yml | 4 - locales/no-NO.yml | 6 - locales/pl-PL.yml | 3 - locales/ru-RU.yml | 6 - locales/sk-SK.yml | 3 - locales/th-TH.yml | 3 - locales/tr-TR.yml | 6 - locales/uk-UA.yml | 6 - locales/vi-VN.yml | 5 - locales/zh-CN.yml | 4 - locales/zh-TW.yml | 4 - packages/backend/src/server/api/endpoints.ts | 2 - .../src/server/api/endpoints/patrons.ts | 33 ----- packages/client/src/pages/user/home.vue | 28 ----- patrons.json | 118 ------------------ 27 files changed, 4 insertions(+), 284 deletions(-) delete mode 100644 packages/backend/src/server/api/endpoints/patrons.ts delete mode 100644 patrons.json diff --git a/docs/api-change.md b/docs/api-change.md index fc82e12806..e788b3d637 100644 --- a/docs/api-change.md +++ b/docs/api-change.md @@ -2,6 +2,10 @@ Breaking changes are indicated by the :warning: icon. +## Unreleased + +- :warning: Removed `patrons` endpoint. + ## v20240405 - Added `notes/history` endpoint. diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 35fe237190..622074056c 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -893,9 +893,6 @@ _aboutFirefish: source: "الشفرة المصدرية" translation: "ترجم ميسكي" donate: "تبرع لميسكي" - morePatrons: "نحن نقدر الدعم الذي قدمه العديد من الأشخاص الذين لم نذكرهم. شكرًا - لكم 🥰" - patrons: "الداعمون" _nsfw: respect: "اخف الوسائط ذات المحتوى الحساس" ignore: "اعرض الوسائط ذات المحتوى الحساس" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index d0869736ef..d17766b499 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -975,8 +975,6 @@ _aboutFirefish: source: "সোর্স কোড" translation: "Firefish অনুবাদ করুন" donate: "Firefish তে দান করুন" - morePatrons: "আরও অনেকে আমাদের সাহায্য করছেন। তাদের সবাইকে ধন্যবাদ 🥰" - patrons: "সমর্থনকারী" _nsfw: respect: "স্পর্শকাতর মিডিয়া লুকান" ignore: "স্পর্শকাতর মিডিয়া লুকাবেন না" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 78ddcc2f55..47328cb554 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1586,18 +1586,12 @@ _aboutFirefish: translation: Tradueix Firefish about: Firefish és una bifurcació de Misskey feta per ThatOneCalculator, que està en desenvolupament des del 2022. - morePatrons: També agraïm el suport de molts altres ajudants que no figuren aquí. - Gràcies! 🥰 - patrons: Mecenes de Firefish - patronsList: Llistats cronològicament, no per la quantitat donada. Fes una donació - amb l'enllaç de dalt per veure el teu nom aquí! donateTitle: T'agrada Firefish? pleaseDonateToFirefish: Penseu en fer una donació a Firefish per donar suport al seu desenvolupament. pleaseDonateToHost: Penseu també en fer una donació a la vostre instància, {host}, per ajudar-lo a suportar els costos de funcionament. donateHost: Fes una donació a {host} - sponsors: Patrocinadors de Calckey misskeyContributors: Col·laboradors de Misskey unknown: Desconegut pageLikesCount: Nombre de pàgines amb M'agrada diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 10901246e1..012f1ef3ea 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1092,17 +1092,11 @@ _aboutFirefish: source: "Quellcode" translation: "Firefish übersetzen" donate: "An Firefish spenden" - morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter - Personen sehr. Danke! 🥰" - patrons: "UnterstützerInnen" - patronsList: Auflistung chonologisch, nicht nach Spenden-Größe. Spende über den - Link oben, um hier aufgeführt zu werden! donateTitle: Gefällt dir Firefish? pleaseDonateToFirefish: Bitte erwäge eine Spende an Firefish, um dessen Entwicklung zu unterstützen. pleaseDonateToHost: Bitte erwäge auch, an deinen Heimatserver {host} zu spenden, um bei der Deckung der Betriebskosten zu helfen. - sponsors: Firefish-Sponsoren donateHost: Spende an {host} misskeyContributors: Misskey-Mitwirkende _nsfw: diff --git a/locales/en-US.yml b/locales/en-US.yml index f8da18450c..d552e1e3a2 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1336,12 +1336,6 @@ _aboutFirefish: pleaseDonateToHost: "Please also consider donating to your home server, {host}, to help support its operation costs." donateHost: "Donate to {host}" - morePatrons: "We also appreciate the support of many other helpers not listed here. - Thank you! 🥰" - sponsors: "Firefish sponsors" - patrons: "Firefish patrons" - patronsList: "Listed chronologically, not by donation size. Donate with the link - above to get your name on here!" _nsfw: respect: "Hide NSFW media" ignore: "Don't hide NSFW media" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index bfc2268a27..9980607521 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -1073,17 +1073,11 @@ _aboutFirefish: source: "Código fuente" translation: "Traducir Firefish" donate: "Donar a Firefish" - morePatrons: "También apreciamos el apoyo de muchos más que no están enlistados - aquí. ¡Gracias! 🥰" - patrons: "Mecenas de Firefish" pleaseDonateToFirefish: Por favor considera donar a Firefish para apollar su desarrollo. donateHost: Dona a {host} - patronsList: Listados cronológicamente no por monto de la donación. ¡Dona con el - vínculo de arriba para que tu nombre aparezca aquí! donateTitle: ¿Te gusta Firefish? pleaseDonateToHost: También considera donar a tu propio servidor , {host}, para ayudar con los costos de operación. - sponsors: Patrocinadores de Firefish misskeyContributors: Contribuidores de Misskey _nsfw: respect: "Ocultar medios NSFW" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index da56856077..9dff23f110 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -996,18 +996,12 @@ _aboutFirefish: source: "Code source" translation: "Traduire Firefish" donate: "Soutenir Firefish" - morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes - non mentionnées ici. Merci à toutes et à tous ! 🥰" - patrons: "Contributeurs" pleaseDonateToFirefish: Merci de considérer de faire un don pour soutenir le développement de Firefish. - sponsors: Sponsors Firefish donateTitle: Firefish vous plaît ? pleaseDonateToHost: Également, veuillez envisager de faire un don à votre serveur d'accueil, {host}, pour contribuer à couvrir ses frais de fonctionnement. donateHost: Faire un don à {host} - patronsList: Listé chronologiquement, pas par taille de donation. Faite un don avec - le lien ci-dessus pour avoir votre nom affiché ici ! misskeyContributors: Contributeurs Misskey _nsfw: respect: "Cacher les médias marqués comme contenu sensible (NSFW)" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 20efbdc458..4535492e7d 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -985,12 +985,6 @@ _aboutFirefish: source: "Sumber kode" translation: "Terjemahkan Firefish" donate: "Donasi ke Firefish" - morePatrons: "Kami sangat mengapresiasi dukungan dari banyak penolong lain yang - tidak tercantum disini. Terima kasih! 🥰" - patrons: "Pendukung" - patronsList: Diurutkan secara kronologis, bukan berdasarkan jumlah donasi. Berdonasilah - dengan tautan di atas supaya nama kamu ada di sini! - sponsors: Sponsor Firefish donateTitle: Suka Firefish? pleaseDonateToFirefish: Silakan pertimbangkan berdonasi ke Firefish untuk mendukung pengembangannya. diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 2bd5d1e1a2..e686df5890 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -934,18 +934,12 @@ _aboutFirefish: source: "Codice sorgente" translation: "Traduzione di Firefish" donate: "Sostieni Firefish" - morePatrons: "Apprezziamo sinceramente l'aiuto di tante altre persone non elencate - qui. Grazie mille! 🥰" - patrons: "Sostenitori" - sponsors: Gli sponsor di Firefish misskeyContributors: Contributori di Misskey donateTitle: Ti piace Firefish? pleaseDonateToFirefish: Con una donazione puoi supportare lo sviluppo di Firefish. pleaseDonateToHost: Considera anche una donazione al server che ti ospita, {host}, per contribuire ai costi che sostiene. donateHost: Dona a {host} - patronsList: Elencati in ordine cronologico, non per importo. Dona con il link sopra - per apparire in questa lista! _nsfw: respect: "Nascondi i media sensibli (NSFW)" ignore: "Mostra i media sensibili (NSFW)" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 372dddf579..3f900d1a5e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1109,14 +1109,10 @@ _aboutFirefish: source: "ソースコード" translation: "Firefishを翻訳" donate: "Firefishに寄付" - morePatrons: "他にも多くの方が支援してくれています。ありがとうございます! 🥰" - patrons: "支援者" - patronsList: 寄付額ではなく時系列順に並んでいます。上記のリンクから寄付を行ってここにあなたのIDを載せましょう! pleaseDonateToFirefish: Firefish開発への寄付をご検討ください。 pleaseDonateToHost: また、このサーバー {host} の運営者への寄付もご検討ください。 donateHost: '{host} に寄付する' donateTitle: Firefishを気に入りましたか? - sponsors: Firefish の支援者 _nsfw: respect: "閲覧注意のメディアは隠す" ignore: "閲覧注意のメディアを隠さない" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 8a39260db1..970a27d0ed 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -874,8 +874,6 @@ _aboutFirefish: source: "ソースコード" translation: "Firefishを翻訳" donate: "Firefishに寄付" - morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰" - patrons: "支援者" misskeyContributors: フォーク元のMisskeyを作らはった人ら _mfm: cheatSheet: "MFMチートシート" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index d9750a53df..d67e37d4cf 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -992,10 +992,6 @@ _aboutFirefish: source: "소스 코드" translation: "Firefish를 번역하기" donate: "Firefish에 기부하기" - morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰" - patrons: "후원자" - patronsList: 기부 금액이 아닌 시간 순서로 정렬합니다. 위 링크를 통해 후원하여 당신의 이름을 새겨 보세요! - sponsors: Firefish 스폰서 pleaseDonateToHost: 또한, 이 서버 {host} 의 운영자에게 기부하는 것도 검토하여 주십시오. pleaseDonateToFirefish: Firefish의 개발에 후원하는 것을 검토하여 주십시오. donateHost: '{host} 에게 기부하기' diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 1321d46a3f..b446ff4359 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -987,8 +987,6 @@ _aboutFirefish: pleaseDonateToFirefish: Du kan vurdere å donere en slant til Firefish for å støtte videre utvikling og feilretting. donateHost: Donér til {host} - morePatrons: Vi er også takknemlige for bidragene fra mange andre som ikke er listet - her. Takk til dere alle! 🥰 contributors: Hovedutviklere source: Kildekode allContributors: Alle bidragsytere @@ -996,10 +994,6 @@ _aboutFirefish: pleaseDonateToHost: Du kan også vurdere å donere til hjemme-tjeneren din, {host}, for å hjelpe dem med driftskostnadene for tjenesten. about: Firefish ble opprettet av ThatOneCalculator i 2022, basert på Misskey. - sponsors: Firefishs sponsorer - patrons: Firefishs patroner - patronsList: Listen er kronologisk, ikke etter donert beløp. Doner med lenken over - for å få navnet ditt her! isBot: Denne kontoen er en bot _nsfw: respect: Skjul NSFW-merket media diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index fb57556f06..43eeb3c8f0 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -990,9 +990,6 @@ _aboutFirefish: source: "Kod źródłowy" translation: "Tłumacz Firefish" donate: "Przekaż darowiznę na Firefish" - morePatrons: "Naprawdę doceniam wsparcie ze strony wielu niewymienionych tu osób. - Dziękuję! 🥰" - patrons: "Wspierający" _nsfw: respect: "Ukrywaj media NSFW" ignore: "Nie ukrywaj mediów NSFW" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 2650aa714a..5c81e19a04 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -986,12 +986,6 @@ _aboutFirefish: source: "Исходный код" translation: "Перевод Firefish" donate: "Пожертвование на Firefish" - morePatrons: "Большое спасибо и многим другим, кто принял участие в этом проекте! - 🥰" - patrons: "Материальная поддержка" - patronsList: Перечислены в хронологическом порядке, а не по размеру пожертвования. - Сделайте взнос по ссылке выше, чтобы ваше имя было здесь! - sponsors: Спонсоры Firefish donateTitle: Понравился Firefish? pleaseDonateToFirefish: Пожалуйста, поддержите разработку Firefish. pleaseDonateToHost: Также не забудьте поддержать ваш домашний сервер {host}, чтобы diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 71513af387..e1ed198e4e 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1036,9 +1036,6 @@ _aboutFirefish: source: "Zdrojový kód" translation: "Preložiť Firefish" donate: "Podporiť Firefish" - morePatrons: "Takisto oceňujeme podporu mnoých ďalších, ktorí tu nie sú uvedení. - Ďakujeme! 🥰" - patrons: "Prispievatelia" _nsfw: respect: "Skryť NSFW médiá" ignore: "Neskrývať NSFW médiá" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index fa096a2c4d..4a668f910a 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1022,9 +1022,6 @@ _aboutFirefish: source: "ซอร์สโค้ด" translation: "รับแปลภาษา Firefish" donate: "บริจาคให้กับ Firefish" - morePatrons: "เราขอขอบคุณสำหรับความช่วยเหลือจากผู้ช่วยอื่นๆ ที่ไม่ได้ระบุไว้ที่นี่นะ - ขอขอบคุณ! 🥰" - patrons: "สมาชิกพันธมิตร" _nsfw: respect: "ซ่อนสื่อ NSFW" ignore: "อย่าซ่อนสื่อ NSFW" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 148dbb4757..cb36a6a07c 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -1910,14 +1910,9 @@ _preferencesBackups: updatedAt: 'Güncelleme tarihi: {date} {time}' cannotLoad: Yüklenemedi _aboutFirefish: - patronsList: Bağış büyüklüğüne göre değil, kronolojik olarak listelenmiştir. Adınızı - buraya almak için yukarıdaki bağlantıyla bağış yapın! about: Firefish, 2022'den beri geliştirilmekte olan ThatOneCalculator tarafından yapılan bir Misskey çatalıdır. allContributors: Tüm katkıda bulunanlar - patrons: Firefish patronları - morePatrons: Burada listelenmeyen diğer birçok yardımcının desteğini de takdir ediyoruz. - Teşekkür ederim! 🥰 donate: Firefish'e bağışta bulunun contributors: Ana katkıda bulunanlar source: Kaynak Kodu @@ -1928,7 +1923,6 @@ _aboutFirefish: pleaseDonateToHost: İşletme maliyetlerini desteklemek için lütfen ev sunucunuz {host}'a bağış yapmayı da düşünün. donateHost: '{ev sahibi} için bağış yapın' - sponsors: Firefish sponsorları misskeyContributors: Misskey'e katkıda bulunanlar _weekday: saturday: Cumartesi diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 64db5e0022..aa669f596f 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -825,17 +825,11 @@ _aboutFirefish: source: "Вихідний код" translation: "Перекладати Firefish" donate: "Пожертвувати Firefish" - morePatrons: "Ми дуже цінуємо підтримку багатьох інших помічників, не перелічених - тут. Дякуємо! 🥰" - patrons: "Підтримали" - patronsList: Перераховані в хронологічному порядку, а не за розміром пожертви. Зробіть - внесок за посиланням вище, щоб ваше ім'я було тут! donateTitle: Сподобався Firefish? pleaseDonateToFirefish: Будь ласка, підтримайте розробку Firefish. pleaseDonateToHost: Також не забудьте підтримати ваш домашній сервер {host}, щоб допомогти з його операційними витратами. donateHost: Зробити внесок на рахунок {host} - sponsors: Спонсори Firefish misskeyContributors: Контрибутори Misskey _nsfw: respect: "Приховувати NSFW медіа" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index c6899c5b16..4c6a01f1ae 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1050,15 +1050,10 @@ _aboutFirefish: source: "Mã nguồn" translation: "Dịch Firefish" donate: "Ủng hộ Firefish" - morePatrons: "Chúng tôi cũng trân trọng sự hỗ trợ của nhiều người đóng góp khác - không được liệt kê ở đây. Cảm ơn! 🥰" - patrons: "Người ủng hộ" - patronsList: Liệt kê theo thứ tự, không theo số tiền ủng hộ. Hãy để tên bạn ở đây! donateTitle: Thích Firefish? pleaseDonateToFirefish: Hãy cân nhắc ủng hộ Firefish phát triển. donateHost: Ủng hộ {host} pleaseDonateToHost: Cũng như ủng hộ chi phí vận hành máy chủ {host} của bạn. - sponsors: Nhà tài trợ Firefish misskeyContributors: Người đóng góp Misskey _nsfw: respect: "Ẩn nội dung NSFW" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index a8510612dc..c496d05381 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -995,10 +995,6 @@ _aboutFirefish: source: "源代码" translation: "翻译 Firefish" donate: "赞助 Firefish" - morePatrons: "还有很多其它的人也在支持我们,非常感谢🥰" - patrons: "Firefish 赞助者" - patronsList: 按时间顺序而不是捐赠金额排列。通过上面的链接捐款,让您的名字出现在这里! - sponsors: Firefish 赞助者们 donateTitle: 喜欢 Firefish 吗? pleaseDonateToFirefish: 请考虑赞助 Firefish 以支持其开发。 pleaseDonateToHost: 也请考虑赞助您的主服务器 {host},以帮助支持其运营成本。 diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 367449350d..5a722933e6 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -991,10 +991,6 @@ _aboutFirefish: source: "原始碼" translation: "翻譯Firefish" donate: "贊助Firefish" - morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰" - patrons: "贊助者" - patronsList: 按時間順序列出,而不是按贊助規模列出。使用上面的連結贊助,在這裡獲得顯示您名字的機會! - sponsors: Firefish 贊助者們 donateTitle: 覺得 Firefish 棒嗎? pleaseDonateToFirefish: 請考慮向 Firefish 贊助以支持其發展。 pleaseDonateToHost: 還請考慮捐贈給您在使用的伺服器 {host},以支援龐大的運營成本。 diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 9a0de00b8b..734534b3ea 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -286,7 +286,6 @@ import * as ep___pinnedUsers from "./endpoints/pinned-users.js"; import * as ep___customMotd from "./endpoints/custom-motd.js"; import * as ep___customSplashIcons from "./endpoints/custom-splash-icons.js"; import * as ep___latestVersion from "./endpoints/latest-version.js"; -import * as ep___patrons from "./endpoints/patrons.js"; import * as ep___release from "./endpoints/release.js"; import * as ep___promo_read from "./endpoints/promo/read.js"; import * as ep___requestResetPassword from "./endpoints/request-reset-password.js"; @@ -636,7 +635,6 @@ const eps = [ ["custom-motd", ep___customMotd], ["custom-splash-icons", ep___customSplashIcons], ["latest-version", ep___latestVersion], - ["patrons", ep___patrons], ["release", ep___release], ["promo/read", ep___promo_read], ["request-reset-password", ep___requestResetPassword], diff --git a/packages/backend/src/server/api/endpoints/patrons.ts b/packages/backend/src/server/api/endpoints/patrons.ts deleted file mode 100644 index 7da72eb81e..0000000000 --- a/packages/backend/src/server/api/endpoints/patrons.ts +++ /dev/null @@ -1,33 +0,0 @@ -import define from "@/server/api/define.js"; -import * as fs from "node:fs/promises"; -import { fileURLToPath } from "node:url"; -import { dirname } from "node:path"; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -export const meta = { - tags: ["meta"], - description: "Get Firefish patrons", - - requireCredential: false, - requireCredentialPrivateMode: false, -} as const; - -export const paramDef = { - type: "object", - properties: { - forceUpdate: { type: "boolean", default: false }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps) => { - const patrons = JSON.parse( - await fs.readFile(`${_dirname}/../../../../../../patrons.json`, "utf-8"), - ); - return { - patrons: patrons.patrons, - sponsors: patrons.sponsors, - }; -}); diff --git a/packages/client/src/pages/user/home.vue b/packages/client/src/pages/user/home.vue index 13846e42d4..463e5cb4a2 100644 --- a/packages/client/src/pages/user/home.vue +++ b/packages/client/src/pages/user/home.vue @@ -101,18 +101,6 @@ v-tooltip.noDelay="i18n.ts.isBot" ><i :class="icon('ph-robot')"></i ></span> - <span - v-if=" - patrons?.includes( - `@${user.username}@${ - user.host || host - }`, - ) - " - v-tooltip.noDelay="i18n.ts.isPatron" - style="color: var(--badge)" - ><i :class="icon('ph-hand-coins')"></i - ></span> </div> </div> </div> @@ -188,18 +176,6 @@ v-tooltip.noDelay="i18n.ts.isBot" ><i :class="icon('ph-robot')"></i ></span> - <span - v-if=" - patrons?.includes( - `@${user.username}@${ - user.host || host - }`, - ) - " - v-tooltip.noDelay="i18n.ts.isPatron" - style="color: var(--badge)" - ><i :class="icon('ph-hand-coins')"></i - ></span> </div> </div> <div class="follow-container"> @@ -406,7 +382,6 @@ const parallaxAnimationId = ref<null | number>(null); const narrow = ref<null | boolean>(null); const rootEl = ref<null | HTMLElement>(null); const bannerEl = ref<null | HTMLElement>(null); -const patrons = ref([]); const age = computed(() => { return calcAge(props.user.birthday); @@ -452,9 +427,6 @@ const timeForThem = computed(() => { return ""; }); -const patronsResp = await os.api("patrons"); -patrons.value = patronsResp.patrons; - function parallaxLoop() { parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop); parallax(); diff --git a/patrons.json b/patrons.json deleted file mode 100644 index 738083ec98..0000000000 --- a/patrons.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "patrons": [ - "@atomicpoet@firefish.social", - "@shoq@mastodon.social", - "@pikadude@erisly.social", - "@sage@stop.voring.me", - "@sky@therian.club", - "@panos@electricrequiem.com", - "@redhunt07@www.foxyhole.io", - "@griff@firefish.social", - "@cafkafk@ck.cafkafk.com", - "@privateger@plasmatrap.com", - "@effye@toot.thoughtworks.com", - "@Kio@kitsunes.club", - "@twann@tech.lgbt", - "@surfbum@firefish.nz", - "@topher@mastodon.online", - "@hanicef@stop.voring.me", - "@nmkj@calckey.jp", - "@unattributed@firefish.social", - "@cody@misskey.codingneko.com", - "@kate@blahaj.zone", - "@emtk@mkkey.net", - "@jovikowi@firefish.social", - "@padraig@firefish.social", - "@pancakes@cats.city", - "@theresmiling@firefish.social", - "@kristian@firefish.social", - "@jo@blahaj.zone", - "@narF@firefish.social", - "@AlderForrest@raining.anvil.top", - "@box464@firefish.social", - "@MariaTheMartian@firefish.social", - "@nisemikol@firefish.social", - "@smallpatatas@blahaj.zone", - "@bayra@stop.voring.me", - "@frost@wolfdo.gg", - "@joebiden@fuckgov.org", - "@nyaa@firefish.social", - "@Dan@firefish.social", - "@dana@firefish.social", - "@Jdreben@firefish.social", - "@natalie@prismst.one", - "@KelsonV@wandering.shop", - "@breakfastmtn@firefish.social", - "@richardazia@mastodon.social", - "@joestone@firefish.social", - "@aj@firefish.social", - "@zepfanman@ramblingreaders.org", - "@kimby@stop.voring.me", - "@fyrfli@fyrfli.social", - "@riversidebryan@firefish.lgbt", - "@aRubes@sloth.run", - "@andreasdotorg@firefish.social", - "@ozzy@calckey.online", - "@leni@windycity.style", - "@mhzmodels@calckey.art", - "@ReflexVE@firefish.social", - "@mark@firefish.social", - "@skyizwhite@himagine.club", - "@Uwu@firefish.social", - "@jGoose@firefish.social", - "@kunev@blewsky.social", - "@Simoto@electricrequiem.com", - "@Evoterra@firefish.social", - "@LauraLangdon@procial.tchncs.de", - "@mho@social.heise.de", - "@richardazia@firefish.social", - "@blues653@firefish.social", - "@rafale_blue@calc.04.si", - "@esm@lethallava.land", - "@vmstan@vmst.io", - "@jtbennett@noc.social", - "@renere@distance.blue", - "@theking@kitsunes.club", - "@toof@fedi.toofie.net", - "@Punko@firefish.social", - "@joesbrat67@firefish.social", - "@arth@firefish.social", - "@octofloofy@ck.octofloofy.ink", - "@pauliehedron@infosec.town", - "@soulthunk@lethallava.land", - "@bumble@ibe.social", - "@DarrenNevares@firefish.social", - "@irfan@firefish.social", - "@dvd@dvd.chat", - "@charlie2alpha@electricrequiem.com", - "@arndot@layer8.space", - "@ryan@c.ryanccn.dev", - "@lapastora_deprova@firefish.social", - "@rameez@firefish.social", - "@dracoling@firetribe.org", - "@Space6host@firefish.social", - "@zakalwe@plasmatrap.com", - "@seasicksailor@firefish.social", - "@geerue@firefish.social", - "@WXFanatic@m.ai6yr.org", - "@Hunkabilly@calckey.world", - "@samleegray@firefish.social", - "@schwarzewald@kodow.net", - "@Conatusprinciple@firefish.social", - "@183231bcb@firefish.lgbt", - "@wiase@firefish.social", - "@leonieke@vitaulium.nl", - "@soulfire@wackywolf.xyz", - "@elbullazul@pub.elbullazul.com", - "@rafale_blue@calc.04.si", - "@firnin@federation.network", - "@clement@ck.villisek.fr", - "@hryggrbyr@ibe.social" - ], - "sponsors": [ - "@atomicpoet@firefish.social", - "@unattributed@firefish.social", - "@jtbennett@noc.social", - "\nInterkosmos Link" - ] -} From 008d8d8f5c3d761552d0a27953261f0a77ed9916 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sat, 13 Apr 2024 21:30:38 +0900 Subject: [PATCH 045/110] docs: update changelog --- docs/changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index f0076197bc..85f13c6238 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,6 +8,9 @@ Critical security updates are indicated by the :warning: icon. ## Unreleased - Add "Media" tab to user page +- Improve federation and rendering of mathematical expressions +- Remove donor information from the web client + - See also: https://info.firefish.dev/notes/9s1n283sb10rh869 - Fix bugs ## [v20240405](https://firefish.dev/firefish/firefish/-/merge_requests/10733/commits) From 5ba88f3d6f005c895528399504daddbfa7cba9d0 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sat, 13 Apr 2024 21:34:20 +0900 Subject: [PATCH 046/110] v20240413 --- docs/api-change.md | 2 +- docs/changelog.md | 2 +- docs/notice-for-admins.md | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-change.md b/docs/api-change.md index e788b3d637..f3ed584c32 100644 --- a/docs/api-change.md +++ b/docs/api-change.md @@ -2,7 +2,7 @@ Breaking changes are indicated by the :warning: icon. -## Unreleased +## v20240413 - :warning: Removed `patrons` endpoint. diff --git a/docs/changelog.md b/docs/changelog.md index 85f13c6238..a818e09835 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,7 +5,7 @@ Critical security updates are indicated by the :warning: icon. - Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well. - Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well. -## Unreleased +## [v20240413](https://firefish.dev/firefish/firefish/-/merge_requests/10741/commits) - Add "Media" tab to user page - Improve federation and rendering of mathematical expressions diff --git a/docs/notice-for-admins.md b/docs/notice-for-admins.md index c22b1da0aa..9bff40a65c 100644 --- a/docs/notice-for-admins.md +++ b/docs/notice-for-admins.md @@ -2,7 +2,7 @@ You can skip intermediate versions when upgrading from an old version, but please read the notices and follow the instructions for each intermediate version before [upgrading](./upgrade.md). -## Unreleased +## v20240413 ### For all users diff --git a/package.json b/package.json index bff235df6e..a7bb0a5cc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firefish", - "version": "20240405", + "version": "20240413", "repository": { "type": "git", "url": "https://firefish.dev/firefish/firefish.git" From 3e43819ba1d0e1d0c2db654655c2e03eee5a8294 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Sat, 13 Apr 2024 23:08:46 +0800 Subject: [PATCH 047/110] rewrite MkFormDialog --- .../client/src/components/MkFormDialog.vue | 195 +++++++++--------- packages/client/src/os.ts | 17 +- packages/client/src/types/form.ts | 123 +++++++++++ 3 files changed, 228 insertions(+), 107 deletions(-) create mode 100644 packages/client/src/types/form.ts diff --git a/packages/client/src/components/MkFormDialog.vue b/packages/client/src/components/MkFormDialog.vue index 11f1d63e29..6007ade2f2 100644 --- a/packages/client/src/components/MkFormDialog.vue +++ b/packages/client/src/components/MkFormDialog.vue @@ -8,7 +8,7 @@ @click="cancel()" @ok="ok()" @close="cancel()" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header> {{ title }} @@ -17,86 +17,84 @@ <MkSpacer :margin-min="20" :margin-max="32"> <div class="_formRoot"> <template - v-for="item in Object.keys(form).filter( - (item) => !form[item].hidden, - )" + v-for="[formItem, formItemName] in unHiddenForms()" > <FormInput - v-if="form[item].type === 'number'" - v-model="values[item]" + v-if="formItem.type === 'number'" + v-model="values[formItemName]" type="number" - :step="form[item].step || 1" + :step="formItem.step || 1" class="_formBlock" > <template #label - ><span v-text="form[item].label || item"></span - ><span v-if="form[item].required === false"> + ><span v-text="formItem.label || formItemName"></span + ><span v-if="formItem.required === false"> ({{ i18n.ts.optional }})</span ></template > - <template v-if="form[item].description" #caption>{{ - form[item].description + <template v-if="formItem.description" #caption>{{ + formItem.description }}</template> </FormInput> <FormInput v-else-if=" - form[item].type === 'string' && - !form[item].multiline + formItem.type === 'string' && + !formItem.multiline " - v-model="values[item]" + v-model="values[formItemName]" type="text" class="_formBlock" > <template #label - ><span v-text="form[item].label || item"></span - ><span v-if="form[item].required === false"> + ><span v-text="formItem.label || formItemName"></span + ><span v-if="formItem.required === false"> ({{ i18n.ts.optional }})</span ></template > - <template v-if="form[item].description" #caption>{{ - form[item].description + <template v-if="formItem.description" #caption>{{ + formItem.description }}</template> </FormInput> <FormTextarea v-else-if=" - form[item].type === 'string' && form[item].multiline + formItem.type === 'string' && formItem.multiline " - v-model="values[item]" + v-model="values[formItemName]" class="_formBlock" > <template #label - ><span v-text="form[item].label || item"></span - ><span v-if="form[item].required === false"> + ><span v-text="formItem.label || formItemName"></span + ><span v-if="formItem.required === false"> ({{ i18n.ts.optional }})</span ></template > - <template v-if="form[item].description" #caption>{{ - form[item].description + <template v-if="formItem.description" #caption>{{ + formItem.description }}</template> </FormTextarea> <FormSwitch - v-else-if="form[item].type === 'boolean'" - v-model="values[item]" + v-else-if="formItem.type === 'boolean'" + v-model="values[formItemName]" class="_formBlock" > - <span v-text="form[item].label || item"></span> - <template v-if="form[item].description" #caption>{{ - form[item].description + <span v-text="formItem.label || formItemName"></span> + <template v-if="formItem.description" #caption>{{ + formItem.description }}</template> </FormSwitch> <FormSelect - v-else-if="form[item].type === 'enum'" - v-model="values[item]" + v-else-if="formItem.type === 'enum'" + v-model="values[formItemName]" class="_formBlock" > - <template #label - ><span v-text="form[item].label || item"></span - ><span v-if="form[item].required === false"> + <template #label> + <span v-text="formItem.label || formItemName"></span> + <span v-if="formItem.required === false"> ({{ i18n.ts.optional }})</span - ></template > + </template> <option - v-for="item in form[item].enum" + v-for="item in formItem.enum" :key="item.value" :value="item.value" > @@ -104,18 +102,18 @@ </option> </FormSelect> <FormRadios - v-else-if="form[item].type === 'radio'" - v-model="values[item]" + v-else-if="formItem.type === 'radio'" + v-model="values[formItemName]" class="_formBlock" > <template #label - ><span v-text="form[item].label || item"></span - ><span v-if="form[item].required === false"> + ><span v-text="formItem.label || formItemName"></span + ><span v-if="formItem.required === false"> ({{ i18n.ts.optional }})</span ></template > <option - v-for="item in form[item].options" + v-for="item in formItem.options" :key="item.value" :value="item.value" > @@ -123,30 +121,30 @@ </option> </FormRadios> <FormRange - v-else-if="form[item].type === 'range'" - v-model="values[item]" - :min="form[item].min" - :max="form[item].max" - :step="form[item].step" - :text-converter="form[item].textConverter" + v-else-if="formItem.type === 'range'" + v-model="values[formItemName]" + :min="formItem.min" + :max="formItem.max" + :step="formItem.step" + :text-converter="formItem.textConverter" class="_formBlock" > <template #label - ><span v-text="form[item].label || item"></span - ><span v-if="form[item].required === false"> + ><span v-text="formItem.label || formItemName"></span + ><span v-if="formItem.required === false"> ({{ i18n.ts.optional }})</span ></template > - <template v-if="form[item].description" #caption>{{ - form[item].description + <template v-if="formItem.description" #caption>{{ + formItem.description }}</template> </FormRange> <MkButton - v-else-if="form[item].type === 'button'" + v-else-if="formItem.type === 'button'" class="_formBlock" - @click="form[item].action($event, values)" + @click="formItem.action($event, values)" > - <span v-text="form[item].content || item"></span> + <span v-text="formItem.content || formItemName"></span> </MkButton> </template> </div> @@ -154,8 +152,8 @@ </XModalWindow> </template> -<script lang="ts"> -import { defineComponent } from "vue"; +<script lang="ts" setup> +import { ref } from "vue"; import FormInput from "./form/input.vue"; import FormTextarea from "./form/textarea.vue"; import FormSwitch from "./form/switch.vue"; @@ -165,59 +163,50 @@ import MkButton from "./MkButton.vue"; import FormRadios from "./form/radios.vue"; import XModalWindow from "@/components/MkModalWindow.vue"; import { i18n } from "@/i18n"; +import type { FormItemType } from "@/types/form"; -export default defineComponent({ - components: { - XModalWindow, - FormInput, - FormTextarea, - FormSwitch, - FormSelect, - FormRange, - MkButton, - FormRadios, - }, +const props = defineProps<{ + title: string; + form: Record<string, FormItemType>; +}>(); - props: { - title: { - type: String, - required: true, +// biome-ignore lint/suspicious/noExplicitAny: To prevent overly complex types we have to use any here +type ValueType = Record<string, any>; + +const emit = defineEmits<{ + done: [ + status: { + result?: Record<string, FormItemType["default"]>; + canceled?: true; }, - form: { - type: Object, - required: true, - }, - }, + ]; + closed: []; +}>(); - emits: ["done"], +const values = ref<ValueType>({}); +const dialog = ref<InstanceType<typeof XModalWindow> | null>(null); - data() { - return { - values: {}, - i18n, - }; - }, +for (const item in props.form) { + values.value[item] = props.form[item].default ?? null; +} - created() { - for (const item in this.form) { - this.values[item] = this.form[item].default ?? null; - } - }, +function unHiddenForms(): [FormItemType, string][] { + return Object.keys(props.form) + .filter((itemName) => !props.form[itemName].hidden) + .map((itemName) => [props.form[itemName], itemName]); +} - methods: { - ok() { - this.$emit("done", { - result: this.values, - }); - this.$refs.dialog.close(); - }, +function ok() { + emit("done", { + result: values.value, + }); + dialog.value!.close(); +} - cancel() { - this.$emit("done", { - canceled: true, - }); - this.$refs.dialog.close(); - }, - }, -}); +function cancel() { + emit("done", { + canceled: true, + }); + dialog.value!.close(); +} </script> diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index ac1227b827..b7a17097e3 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -13,6 +13,7 @@ import MkWaitingDialog from "@/components/MkWaitingDialog.vue"; import { apiUrl, url } from "@/config"; import { me } from "@/me"; import type { MenuItem } from "@/types/menu"; +import type { FormItemType, GetFormResultType } from "@/types/form"; export const pendingApiRequestsCount = ref(0); @@ -611,8 +612,16 @@ export function waiting(): Promise<void> { }); } -export function form(title, form) { - return new Promise((resolve, reject) => { +export function form<T extends Record<string, FormItemType>>( + title: string, + form: T, +) { + return new Promise<{ + result?: { + [K in keyof T]: GetFormResultType<T[K]["type"]>; + }; + canceled?: true; + }>((resolve, _reject) => { popup( defineAsyncComponent({ loader: () => import("@/components/MkFormDialog.vue"), @@ -622,7 +631,7 @@ export function form(title, form) { { title, form }, { done: (result) => { - resolve(result); + resolve(result as never); }, }, "closed", @@ -631,7 +640,7 @@ export function form(title, form) { } export async function selectUser() { - return new Promise((resolve, reject) => { + return new Promise<entities.UserDetailed>((resolve, reject) => { popup( defineAsyncComponent({ loader: () => import("@/components/MkUserSelectDialog.vue"), diff --git a/packages/client/src/types/form.ts b/packages/client/src/types/form.ts new file mode 100644 index 0000000000..9c34df30dc --- /dev/null +++ b/packages/client/src/types/form.ts @@ -0,0 +1,123 @@ +export type BaseFormItem = { + hidden?: boolean; + label?: string; + description?: string; + required?: boolean; +}; + +export type FormItemTextInput = BaseFormItem & { + type: "string"; + default?: string | null; + multiline?: false; +}; +export type FormItemTextarea = BaseFormItem & { + type: "string"; + default?: string | null; + multiline: true; +}; + +export type FormItemText = FormItemTextInput | FormItemTextarea; + +export type FormItemNumber = BaseFormItem & { + type: "number"; + default?: number | null; + step?: number | null; +}; +export type FormItemEmail = BaseFormItem & { + type: "email"; + default?: string | null; +}; +export type FormItemPassword = BaseFormItem & { + type: "password"; + default?: never; + __result_typedef?: string; +}; +export type FormItemUrl = BaseFormItem & { + type: "url"; + default?: string | null; +}; +export type FormItemDate = BaseFormItem & { + type: "date"; + default?: Date | null; +}; +export type FormItemTime = BaseFormItem & { + type: "time"; + default?: number | Date | null; +}; +export type FormItemSearch = BaseFormItem & { + type: "search"; + default?: string | null; +}; +export type FormItemSwitch = BaseFormItem & { + type: "boolean"; + default?: boolean | null; +}; +export type FormItemSelect = BaseFormItem & { + type: "enum"; + default?: string | null; + enum: { + value: string | number | symbol | undefined; + label: string; + }[]; +}; +export type FormItemRadios = BaseFormItem & { + type: "radio"; + default?: string | number | symbol | undefined | null; + options: { + label: string; + value: string | number | symbol | undefined; + }[]; +}; +export type FormItemRange = BaseFormItem & { + type: "range"; + default?: number | null; + min: number; + max: number; + step?: number; + textConverter?: (value: number) => string; +}; +export type FormItemButton = BaseFormItem & { + type: "button"; + content?: string; + action: (event, values) => unknown; + default?: never; +}; +export type FormItemObject = BaseFormItem & { + type: "object"; + default: Record<string, unknown> | null; + hidden: true; +}; + +export type FormItemInputArray = [ + FormItemTextInput, + FormItemNumber, + FormItemEmail, + FormItemPassword, + FormItemUrl, + FormItemDate, + FormItemTime, + FormItemSearch, +]; + +export type FormItemTypeArray = [ + ...FormItemInputArray, + FormItemTextarea, + FormItemSwitch, + FormItemSelect, + FormItemButton, + FormItemRadios, + FormItemRange, + FormItemObject, +]; + +export type FormItemInput = FormItemInputArray[number]; + +export type FormItemType = FormItemTypeArray[number]; + +export type GetFormItemByType<T extends FormItemType["type"], F extends FormItemType = FormItemType> = F extends {type: T} ? F : never; + +type NonUndefindAble<T> = T extends undefined ? never : T; + +export type GetFormResultType<T extends FormItemType["type"], I extends FormItemType = GetFormItemByType<T>> = NonUndefindAble< + "__result_typedef" extends keyof I ? I["__result_typedef"] : I["default"] +> From 16880b1231146b0c73460917a9884cf9c5455c65 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Sat, 13 Apr 2024 23:08:58 +0800 Subject: [PATCH 048/110] fix types --- packages/client/@types/global.d.ts | 1 + packages/client/src/components/MkNoteSub.vue | 42 +++---- .../client/src/components/MkPollEditor.vue | 8 +- packages/client/src/components/MkPostForm.vue | 111 ++++++++++-------- .../src/components/MkPostFormDialog.vue | 7 +- packages/client/src/components/MkSignup.vue | 7 +- packages/client/src/components/MkTagCloud.vue | 1 + .../src/components/MkVisibilityPicker.vue | 2 +- packages/client/src/components/form/range.vue | 2 +- packages/client/src/pages/clip.vue | 10 +- packages/client/src/store.ts | 8 +- packages/client/src/types/post-form.ts | 12 ++ packages/client/tsconfig.json | 2 +- packages/firefish-js/src/api.types.ts | 16 +-- packages/firefish-js/src/entities.ts | 6 +- packages/firefish-js/src/index.ts | 2 + 16 files changed, 135 insertions(+), 102 deletions(-) create mode 100644 packages/client/src/types/post-form.ts diff --git a/packages/client/@types/global.d.ts b/packages/client/@types/global.d.ts index c757482900..3ac4f09b0c 100644 --- a/packages/client/@types/global.d.ts +++ b/packages/client/@types/global.d.ts @@ -1,3 +1,4 @@ +// biome-ignore lint/suspicious/noExplicitAny: type FIXME = any; declare const _LANGS_: string[][]; diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue index 08c0bf6c38..c7cf06d2e5 100644 --- a/packages/client/src/components/MkNoteSub.vue +++ b/packages/client/src/components/MkNoteSub.vue @@ -1,7 +1,7 @@ <template> <article v-if="!muted.muted || muted.what === 'reply'" - :id="detailedView ? appearNote.id : null" + :id="detailedView ? appearNote.id : undefined" ref="el" v-size="{ max: [450, 500] }" class="wrpstxzv" @@ -35,10 +35,10 @@ :parent-id="parentId" :conversation="conversation" :detailed-view="detailedView" - @focusfooter="footerEl.focus()" + @focusfooter="footerEl!.focus()" /> <div v-if="translating || translation" class="translation"> - <MkLoading v-if="translating" mini /> + <MkLoading v-if="translating || translation == null" mini /> <div v-else class="translated"> <b >{{ @@ -217,6 +217,7 @@ import { useNoteCapture } from "@/scripts/use-note-capture"; import { defaultStore } from "@/store"; import { deepClone } from "@/scripts/clone"; import icon from "@/scripts/icon"; +import type { NoteTranslation } from "@/types/note"; const router = useRouter(); @@ -256,12 +257,12 @@ const isRenote = note.value.fileIds.length === 0 && note.value.poll == null; -const el = ref<HTMLElement>(); -const footerEl = ref<HTMLElement>(); +const el = ref<HTMLElement | null>(null); +const footerEl = ref<HTMLElement | null>(null); const menuButton = ref<HTMLElement>(); -const starButton = ref<InstanceType<typeof XStarButton>>(); -const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); -const reactButton = ref<HTMLElement>(); +const starButton = ref<InstanceType<typeof XStarButton> | null>(null); +const renoteButton = ref<InstanceType<typeof XRenoteButton> | null>(null); +const reactButton = ref<HTMLElement | null>(null); const appearNote = computed(() => isRenote ? (note.value.renote as entities.Note) : note.value, ); @@ -274,7 +275,7 @@ const muted = ref( defaultStore.state.mutedLangs, ), ); -const translation = ref(null); +const translation = ref<NoteTranslation | null>(null); const translating = ref(false); const replies: entities.Note[] = props.conversation @@ -330,21 +331,21 @@ useNoteCapture({ isDeletedRef: isDeleted, }); -function reply(viaKeyboard = false): void { +function reply(_viaKeyboard = false): void { pleaseLogin(); os.post({ reply: appearNote.value, - animation: !viaKeyboard, + // animation: !viaKeyboard, }).then(() => { focus(); }); } -function react(viaKeyboard = false): void { +function react(_viaKeyboard = false): void { pleaseLogin(); blur(); reactionPicker.show( - reactButton.value, + reactButton.value!, (reaction) => { os.api("notes/reactions/create", { noteId: appearNote.value.id, @@ -388,14 +389,15 @@ function menu(viaKeyboard = false): void { } function onContextmenu(ev: MouseEvent): void { - const isLink = (el: HTMLElement) => { + const isLink = (el: HTMLElement | null) => { + if (el == null) return; if (el.tagName === "A") return true; if (el.parentElement) { return isLink(el.parentElement); } }; - if (isLink(ev.target)) return; - if (window.getSelection().toString() !== "") return; + if (isLink(ev.target as HTMLElement | null)) return; + if (window.getSelection()?.toString() !== "") return; if (defaultStore.state.useReactionPickerForContextMenu) { ev.preventDefault(); @@ -454,15 +456,15 @@ function onContextmenu(ev: MouseEvent): void { } function focus() { - el.value.focus(); + el.value!.focus(); } function blur() { - el.value.blur(); + el.value!.blur(); } -function noteClick(e) { - if (document.getSelection().type === "Range" || !expandOnNoteClick) { +function noteClick(e: MouseEvent) { + if (document.getSelection()?.type === "Range" || !expandOnNoteClick) { e.stopPropagation(); } else { router.push(notePage(props.note)); diff --git a/packages/client/src/components/MkPollEditor.vue b/packages/client/src/components/MkPollEditor.vue index b05f80fafa..f9dfcf80a8 100644 --- a/packages/client/src/components/MkPollEditor.vue +++ b/packages/client/src/components/MkPollEditor.vue @@ -84,14 +84,10 @@ import { formatDateTimeString } from "@/scripts/format-time-string"; import { addTime } from "@/scripts/time"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; +import type { PollType } from "@/types/post-form"; const props = defineProps<{ - modelValue: { - expiresAt: string; - expiredAfter: number; - choices: string[]; - multiple: boolean; - }; + modelValue: PollType; }>(); const emit = defineEmits<{ "update:modelValue": [ diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue index 2735eb55a3..0a03dbb30e 100644 --- a/packages/client/src/components/MkPostForm.vue +++ b/packages/client/src/components/MkPostForm.vue @@ -20,7 +20,7 @@ class="account _button" @click="openAccountMenu" > - <MkAvatar :user="postAccount ?? me" class="avatar" /> + <MkAvatar :user="postAccount ?? me!" class="avatar" /> </button> <div class="right"> <span @@ -297,14 +297,22 @@ </template> <script lang="ts" setup> -import { computed, inject, nextTick, onMounted, ref, watch } from "vue"; +import { + type Ref, + computed, + inject, + nextTick, + onMounted, + ref, + watch, +} from "vue"; import * as mfm from "mfm-js"; import autosize from "autosize"; import insertTextAtCursor from "insert-text-at-cursor"; import { length } from "stringz"; import { toASCII } from "punycode/"; import { acct } from "firefish-js"; -import type { entities, languages } from "firefish-js"; +import type { ApiTypes, entities, languages } from "firefish-js"; import { throttle } from "throttle-debounce"; import XNoteSimple from "@/components/MkNoteSimple.vue"; import XNotePreview from "@/components/MkNotePreview.vue"; @@ -341,6 +349,7 @@ import type { MenuItem } from "@/types/menu"; import icon from "@/scripts/icon"; import MkVisibilityPicker from "@/components/MkVisibilityPicker.vue"; import type { NoteVisibility } from "@/types/note"; +import type { NoteDraft, PollType } from "@/types/post-form"; const modal = inject("modal"); @@ -353,11 +362,11 @@ const props = withDefaults( specified?: entities.User; initialText?: string; initialVisibility?: NoteVisibility; - initialLanguage?: typeof languages; + initialLanguage?: (typeof languages)[number]; initialFiles?: entities.DriveFile[]; initialLocalOnly?: boolean; initialVisibleUsers?: entities.User[]; - initialNote?: entities.Note; + initialNote?: NoteDraft; instant?: boolean; fixed?: boolean; autofocus?: boolean; @@ -390,12 +399,7 @@ const showBigPostButton = defaultStore.state.showBigPostButton; const posting = ref(false); const text = ref(props.initialText ?? ""); const files = ref(props.initialFiles ?? ([] as entities.DriveFile[])); -const poll = ref<{ - choices: string[]; - multiple: boolean; - expiresAt: string | null; - expiredAfter: string | null; -} | null>(null); +const poll = ref<PollType | null>(null); const useCw = ref(false); const showPreview = ref(defaultStore.state.showPreviewByDefault); const cw = ref<string | null>(null); @@ -411,12 +415,12 @@ const visibility = ref( : defaultStore.state.defaultNoteVisibility), ); -const visibleUsers = ref([]); +const visibleUsers = ref<entities.User[]>([]); if (props.initialVisibleUsers) { props.initialVisibleUsers.forEach(pushVisibleUser); } const draghover = ref(false); -const quoteId = ref(null); +const quoteId = ref<string | null>(null); const hasNotSpecifiedMentions = ref(false); const recentHashtags = ref( JSON.parse(localStorage.getItem("hashtags") || "[]"), @@ -500,7 +504,9 @@ const canPost = computed((): boolean => { const withHashtags = computed( defaultStore.makeGetterSetter("postFormWithHashtags"), ); -const hashtags = computed(defaultStore.makeGetterSetter("postFormHashtags")); +const hashtags = computed( + defaultStore.makeGetterSetter("postFormHashtags"), +) as Ref<string | null>; watch(text, () => { checkMissingMention(); @@ -525,7 +531,7 @@ if (props.mention) { if ( props.reply && - (props.reply.user.username !== me.username || + (props.reply.user.username !== me!.username || (props.reply.user.host != null && props.reply.user.host !== host)) ) { text.value = `@${props.reply.user.username}${ @@ -545,7 +551,7 @@ if (props.reply && props.reply.text != null) { : `@${x.username}@${toASCII(otherHost)}`; // exclude me - if (me.username === x.username && (x.host == null || x.host === host)) + if (me!.username === x.username && (x.host == null || x.host === host)) continue; // remove duplicates @@ -579,7 +585,7 @@ if ( if (props.reply.visibleUserIds) { os.api("users/show", { userIds: props.reply.visibleUserIds.filter( - (uid) => uid !== me.id && uid !== props.reply.userId, + (uid) => uid !== me!.id && uid !== props.reply!.userId, ), }).then((users) => { users.forEach(pushVisibleUser); @@ -588,7 +594,7 @@ if ( visibility.value = "private"; } - if (props.reply.userId !== me.id) { + if (props.reply.userId !== me!.id) { os.api("users/show", { userId: props.reply.userId }).then((user) => { pushVisibleUser(user); }); @@ -615,7 +621,7 @@ const addRe = (s: string) => { if (defaultStore.state.keepCw && props.reply && props.reply.cw) { useCw.value = true; cw.value = - props.reply.user.username === me.username + props.reply.user.username === me!.username ? props.reply.cw : addRe(props.reply.cw); } @@ -894,11 +900,14 @@ function onCompositionEnd(ev: CompositionEvent) { } async function onPaste(ev: ClipboardEvent) { + if (ev.clipboardData == null) return; + for (const { item, i } of Array.from(ev.clipboardData.items).map( (item, i) => ({ item, i }), )) { if (item.kind === "file") { const file = item.getAsFile(); + if (file == null) continue; const lio = file.name.lastIndexOf("."); const ext = lio >= 0 ? file.name.slice(lio) : ""; const formatted = `${formatTimeString( @@ -911,7 +920,7 @@ async function onPaste(ev: ClipboardEvent) { const paste = ev.clipboardData?.getData("text") ?? ""; - if (!props.renote && !quoteId.value && paste.startsWith(url + "/notes/")) { + if (!props.renote && !quoteId.value && paste.startsWith(`${url}/notes/`)) { ev.preventDefault(); os.yesno({ @@ -919,13 +928,13 @@ async function onPaste(ev: ClipboardEvent) { text: i18n.ts.quoteQuestion, }).then(({ canceled }) => { if (canceled) { - insertTextAtCursor(textareaEl.value, paste); + insertTextAtCursor(textareaEl.value!, paste); return; } quoteId.value = paste .substring(url.length) - .match(/^\/notes\/(.+?)\/?$/)[1]; + .match(/^\/notes\/(.+?)\/?$/)![1]; }); } } @@ -956,16 +965,17 @@ function onDragover(ev) { } } -function onDragenter(ev) { +function onDragenter(_ev) { draghover.value = true; } -function onDragleave(ev) { +function onDragleave(_ev) { draghover.value = false; } -function onDrop(ev): void { +function onDrop(ev: DragEvent): void { draghover.value = false; + if (ev.dataTransfer == null) return; // ファイルだったら if (ev.dataTransfer.files.length > 0) { @@ -1064,7 +1074,7 @@ async function post() { const processedText = preprocess(text.value); - let postData = { + let postData: ApiTypes.NoteSubmitReq = { editId: props.editId ? props.editId : undefined, text: processedText === "" ? undefined : processedText, fileIds: files.value.length > 0 ? files.value.map((f) => f.id) : undefined, @@ -1092,7 +1102,7 @@ async function post() { const hashtags_ = hashtags.value .trim() .split(" ") - .map((x) => (x.startsWith("#") ? x : "#" + x)) + .map((x) => (x.startsWith("#") ? x : `#${x}`)) .join(" "); postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_; } @@ -1104,11 +1114,11 @@ async function post() { } } - let token; + let token: string | undefined; if (postAccount.value) { const storedAccounts = await getAccounts(); - token = storedAccounts.find((x) => x.id === postAccount.value.id)?.token; + token = storedAccounts.find((x) => x.id === postAccount.value!.id)?.token; } posting.value = true; @@ -1119,10 +1129,11 @@ async function post() { deleteDraft(); emit("posted"); if (postData.text && postData.text !== "") { - const hashtags_ = mfm - .parse(postData.text) - .filter((x) => x.type === "hashtag") - .map((x) => x.props.hashtag); + const hashtags_ = ( + mfm + .parse(postData.text) + .filter((x) => x.type === "hashtag") as mfm.MfmHashtag[] + ).map((x) => x.props.hashtag); const history = JSON.parse( localStorage.getItem("hashtags") || "[]", ) as string[]; @@ -1133,7 +1144,7 @@ async function post() { } posting.value = false; postAccount.value = null; - nextTick(() => autosize.update(textareaEl.value)); + nextTick(() => autosize.update(textareaEl.value!)); }); }) .catch((err: { message: string; id: string }) => { @@ -1169,19 +1180,23 @@ function cancel() { function insertMention() { os.selectUser().then((user) => { - insertTextAtCursor(textareaEl.value, "@" + acct.toString(user) + " "); + insertTextAtCursor(textareaEl.value!, `@${acct.toString(user)} `); }); } async function insertEmoji(ev: MouseEvent) { - os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl.value); + os.openEmojiPicker( + (ev.currentTarget ?? ev.target) as HTMLElement, + {}, + textareaEl.value, + ); } async function openCheatSheet(ev: MouseEvent) { os.popup(XCheatSheet, {}, {}, "closed"); } -function showActions(ev) { +function showActions(ev: MouseEvent) { os.popupMenu( postFormActions.map((action) => ({ text: action.title, @@ -1198,7 +1213,7 @@ function showActions(ev) { ); }, })), - ev.currentTarget ?? ev.target, + (ev.currentTarget ?? ev.target) as HTMLElement, ); } @@ -1209,9 +1224,9 @@ function openAccountMenu(ev: MouseEvent) { { withExtraOperation: false, includeCurrentAccount: true, - active: postAccount.value != null ? postAccount.value.id : me.id, + active: postAccount.value != null ? postAccount.value.id : me!.id, onChoose: (account) => { - if (account.id === me.id) { + if (account.id === me!.id) { postAccount.value = null; } else { postAccount.value = account; @@ -1232,14 +1247,14 @@ onMounted(() => { } // TODO: detach when unmount - new Autocomplete(textareaEl.value, text); - new Autocomplete(cwInputEl.value, cw); - new Autocomplete(hashtagsInputEl.value, hashtags); + new Autocomplete(textareaEl.value!, text); + new Autocomplete(cwInputEl.value!, cw as Ref<string>); + new Autocomplete(hashtagsInputEl.value!, hashtags as Ref<string>); - autosize(textareaEl.value); + autosize(textareaEl.value!); nextTick(() => { - autosize(textareaEl.value); + autosize(textareaEl.value!); // 書きかけの投稿を復元 if (!props.instant && !props.mention && !props.specified) { const draft = JSON.parse(localStorage.getItem("drafts") || "{}")[ @@ -1275,8 +1290,8 @@ onMounted(() => { }; } visibility.value = init.visibility; - localOnly.value = init.localOnly; - language.value = init.lang; + localOnly.value = init.localOnly ?? false; + language.value = init.lang ?? null; quoteId.value = init.renote ? init.renote.id : null; } @@ -1289,7 +1304,7 @@ onMounted(() => { } nextTick(() => watchForDraft()); - nextTick(() => autosize.update(textareaEl.value)); + nextTick(() => autosize.update(textareaEl.value!)); }); }); </script> diff --git a/packages/client/src/components/MkPostFormDialog.vue b/packages/client/src/components/MkPostFormDialog.vue index cf6f45e5ed..fafb4afdd4 100644 --- a/packages/client/src/components/MkPostFormDialog.vue +++ b/packages/client/src/components/MkPostFormDialog.vue @@ -25,6 +25,7 @@ import type { entities, languages } from "firefish-js"; import MkModal from "@/components/MkModal.vue"; import MkPostForm from "@/components/MkPostForm.vue"; import type { NoteVisibility } from "@/types/note"; +import type { NoteDraft } from "@/types/post-form"; const props = defineProps<{ reply?: entities.Note; @@ -34,11 +35,11 @@ const props = defineProps<{ specified?: entities.User; initialText?: string; initialVisibility?: NoteVisibility; - initialLanguage?: typeof languages; + initialLanguage?: (typeof languages)[number]; initialFiles?: entities.DriveFile[]; initialLocalOnly?: boolean; initialVisibleUsers?: entities.User[]; - initialNote?: entities.Note; + initialNote?: NoteDraft; instant?: boolean; fixed?: boolean; autofocus?: boolean; @@ -53,7 +54,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>(); const form = shallowRef<InstanceType<typeof MkPostForm>>(); function onPosted() { - modal.value.close({ + modal.value!.close({ useSendAnimation: true, }); } diff --git a/packages/client/src/components/MkSignup.vue b/packages/client/src/components/MkSignup.vue index 9870263b7a..0807935be3 100644 --- a/packages/client/src/components/MkSignup.vue +++ b/packages/client/src/components/MkSignup.vue @@ -319,7 +319,7 @@ const usernameState = ref< | "invalid-format" | "min-range" | "max-range" - >(null); +>(null); const invitationState = ref<null | "entered">(null); const emailState = ref< | null @@ -331,9 +331,10 @@ const emailState = ref< | "unavailable:mx" | "unavailable:smtp" | "unavailable" - | "error">(null); + | "error" +>(null); const passwordStrength = ref<"" | "low" | "medium" | "high">(""); -const passwordRetypeState = ref<null | "match" | "not-match" >(null); +const passwordRetypeState = ref<null | "match" | "not-match">(null); const submitting = ref(false); const ToSAgreement = ref(false); const hCaptchaResponse = ref(null); diff --git a/packages/client/src/components/MkTagCloud.vue b/packages/client/src/components/MkTagCloud.vue index 40dee3c576..d932cbdc8c 100644 --- a/packages/client/src/components/MkTagCloud.vue +++ b/packages/client/src/components/MkTagCloud.vue @@ -76,6 +76,7 @@ onMounted(() => { src: "/client-assets/tagcanvas.min.js", }), ) + // biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially .addEventListener("load", () => (available.value = true)); } }); diff --git a/packages/client/src/components/MkVisibilityPicker.vue b/packages/client/src/components/MkVisibilityPicker.vue index 088da07eb0..bfa8102615 100644 --- a/packages/client/src/components/MkVisibilityPicker.vue +++ b/packages/client/src/components/MkVisibilityPicker.vue @@ -153,7 +153,7 @@ const props = withDefaults( defineProps<{ currentVisibility: NoteVisibility; currentLocalOnly: boolean; - src?: HTMLElement; + src?: HTMLElement | null; }>(), {}, ); diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue index d12596a86c..1377abf034 100644 --- a/packages/client/src/components/form/range.vue +++ b/packages/client/src/components/form/range.vue @@ -48,7 +48,7 @@ const id = os.getUniqueId(); const props = withDefaults( defineProps<{ - modelValue: number; + modelValue: number | null; disabled?: boolean; min: number; max: number; diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue index ffe00beeec..8507721519 100644 --- a/packages/client/src/pages/clip.vue +++ b/packages/client/src/pages/clip.vue @@ -75,29 +75,29 @@ const headerActions = computed(() => icon: `${icon("ph-pencil")}`, text: i18n.ts.toEdit, handler: async (): Promise<void> => { - const { canceled, result } = await os.form(clip.value.name, { + const { canceled, result } = await os.form(clip.value!.name, { name: { type: "string", label: i18n.ts.name, - default: clip.value.name, + default: clip.value!.name, }, description: { type: "string", required: false, multiline: true, label: i18n.ts.description, - default: clip.value.description, + default: clip.value!.description, }, isPublic: { type: "boolean", label: i18n.ts.public, - default: clip.value.isPublic, + default: clip.value!.isPublic, }, }); if (canceled) return; os.apiWithDialog("clips/update", { - clipId: clip.value.id, + clipId: clip.value!.id, ...result, }); }, diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index 85224f0d86..680dba286f 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -2,14 +2,15 @@ import { markRaw, ref } from "vue"; import { isSignedIn } from "./me"; import { Storage } from "./pizzax"; import type { NoteVisibility } from "@/types/note"; +import type { entities, ApiTypes } from "firefish-js"; export const postFormActions: { title: string; - handler: (note: entities.Note) => void | Promise<void>; + handler: (from, update) => void | Promise<void>; }[] = []; export const userActions: { title: string; - handler: (note: entities.Note) => void | Promise<void>; + handler: (user: entities.User) => void | Promise<void>; }[] = []; export const noteActions: { title: string; @@ -19,7 +20,7 @@ export const noteViewInterruptors: { handler: (note: entities.Note) => Promise<entities.Note>; }[] = []; export const notePostInterruptors: { - handler: (note: entities.Note) => Promise<entities.Note>; + handler: (note: ApiTypes.NoteSubmitReq) => Promise<ApiTypes.NoteSubmitReq>; }[] = []; const menuOptions = [ @@ -466,7 +467,6 @@ import darkTheme from "@/themes/d-rosepine.json5"; * Storage for configuration information that does not need to be constantly loaded into memory (non-reactive) */ import lightTheme from "@/themes/l-rosepinedawn.json5"; -import { entities } from "firefish-js"; export class ColdDeviceStorage { public static default = { diff --git a/packages/client/src/types/post-form.ts b/packages/client/src/types/post-form.ts new file mode 100644 index 0000000000..a535ef812f --- /dev/null +++ b/packages/client/src/types/post-form.ts @@ -0,0 +1,12 @@ +import type { entities } from "firefish-js"; + +export type PollType = { + choices: string[]; + multiple: boolean; + expiresAt: string | null; + expiredAfter: number | null; +}; + +export type NoteDraft = entities.Note & { + poll?: PollType; +}; diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 4d62c784dd..51f6e8dd1e 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -30,5 +30,5 @@ "jsx": "preserve" }, "compileOnSave": false, - "include": ["./src/**/*.ts", "./src/**/*.vue"] + "include": ["./src/**/*.ts", "./src/**/*.vue", "./@types"] } diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts index ffb22c191b..30ae1f9cdf 100644 --- a/packages/firefish-js/src/api.types.ts +++ b/packages/firefish-js/src/api.types.ts @@ -36,7 +36,6 @@ import type { UserDetailed, UserGroup, UserList, - UserLite, UserSorting, } from "./entities"; @@ -46,11 +45,13 @@ type TODO = Record<string, any> | null; type NoParams = Record<string, never>; -type ShowUserReq = { username: string; host?: string } | { userId: User["id"] }; +type ShowUserReq = + | { username: string; host?: string | null } + | { userId: User["id"] }; -type NoteSubmitReq = { +export type NoteSubmitReq = { editId?: null | Note["id"]; - visibility?: "public" | "home" | "followers" | "specified"; + visibility?: (typeof consts.noteVisibilities)[number]; visibleUserIds?: User["id"][]; text?: null | string; cw?: null | string; @@ -62,10 +63,11 @@ type NoteSubmitReq = { channelId?: null | Channel["id"]; poll?: null | { choices: string[]; - multiple?: boolean; - expiresAt?: null | number; - expiredAfter?: null | number; + multiple: boolean; + expiresAt: string | null; + expiredAfter: number | null; }; + lang?: string; }; export type Endpoints = { diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index 5858dacb43..457d7ac935 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -29,7 +29,7 @@ export type UserLite = { isIndexable: boolean; isCat?: boolean; speakAsCat?: boolean; - driveCapacityOverrideMb: number | null, + driveCapacityOverrideMb: number | null; }; export type UserDetailed = UserLite & { @@ -238,8 +238,8 @@ export interface RenoteNotification extends BaseNotification { user: User; userId: User["id"]; note: Note & { - renote: Note, - renoteId: string, + renote: Note; + renoteId: string; }; } export interface QuoteNotification extends BaseNotification { diff --git a/packages/firefish-js/src/index.ts b/packages/firefish-js/src/index.ts index 6639985481..3398ed8a2e 100644 --- a/packages/firefish-js/src/index.ts +++ b/packages/firefish-js/src/index.ts @@ -1,6 +1,7 @@ import * as acct from "./acct"; import type { Acct } from "./acct"; import { Endpoints } from "./api.types"; +import type * as ApiTypes from "./api.types"; import * as consts from "./consts"; import Stream, { Connection } from "./streaming"; import * as StreamTypes from "./streaming.types"; @@ -8,6 +9,7 @@ import type * as TypeUtils from "./type-utils"; export { Endpoints, + type ApiTypes, Stream, Connection as ChannelConnection, StreamTypes, From 1a6ba246f22b4d430db8d105fe8ea4c4db3dfc91 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Sat, 13 Apr 2024 23:21:31 +0800 Subject: [PATCH 049/110] add other FormInput --- .../client/src/components/MkFormDialog.vue | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/client/src/components/MkFormDialog.vue b/packages/client/src/components/MkFormDialog.vue index 6007ade2f2..b3d02ef515 100644 --- a/packages/client/src/components/MkFormDialog.vue +++ b/packages/client/src/components/MkFormDialog.vue @@ -55,6 +55,29 @@ formItem.description }}</template> </FormInput> + <FormInput + v-else-if=" + formItem.type === 'email' || + formItem.type === 'password' || + formItem.type === 'url' || + formItem.type === 'date' || + formItem.type === 'time' || + formItem.type === 'search' + " + v-model="values[formItemName]" + :type="formItem.type" + class="_formBlock" + > + <template #label + ><span v-text="formItem.label || formItemName"></span + ><span v-if="formItem.required === false"> + ({{ i18n.ts.optional }})</span + ></template + > + <template v-if="formItem.description" #caption>{{ + formItem.description + }}</template> + </FormInput> <FormTextarea v-else-if=" formItem.type === 'string' && formItem.multiline From aea6659d0bee60b5930155fc3e9eda7cd167b3c8 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 14 Apr 2024 02:35:25 +0900 Subject: [PATCH 050/110] fix: workaround the issue that profile pages don't load if the version is older than 20240212 (v20240212 is vulnerable btw) --- patrons.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 patrons.json diff --git a/patrons.json b/patrons.json new file mode 100644 index 0000000000..cbbeacfdc9 --- /dev/null +++ b/patrons.json @@ -0,0 +1,4 @@ +{ + "patrons": [], + "sponsors": [] +} From cee3a13f510b914f0c67392941739f3a0e53803b Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Sun, 14 Apr 2024 03:15:31 +0800 Subject: [PATCH 051/110] fix types --- packages/client/@types/window.d.ts | 6 + packages/client/src/components/MkDialog.vue | 37 ++-- packages/client/src/components/MkMenu.vue | 3 +- .../client/src/components/MkUrlPreview.vue | 4 +- .../client/src/components/MkWaitingDialog.vue | 8 +- packages/client/src/os.ts | 164 +++++++++++------- .../page-editor/els/page-editor.el.image.vue | 4 +- .../pages/settings/preferences-backups.vue | 4 +- .../client/src/pages/user-list-timeline.vue | 4 +- packages/client/src/scripts/form.ts | 73 +------- packages/client/src/scripts/get-note-menu.ts | 7 +- packages/client/src/scripts/select-file.ts | 35 ++-- packages/client/src/types/form.ts | 29 +++- packages/client/src/widgets/widget.ts | 4 +- packages/firefish-js/src/api.ts | 2 +- packages/firefish-js/src/api.types.ts | 1 + 16 files changed, 194 insertions(+), 191 deletions(-) create mode 100644 packages/client/@types/window.d.ts diff --git a/packages/client/@types/window.d.ts b/packages/client/@types/window.d.ts new file mode 100644 index 0000000000..1ae20c0d20 --- /dev/null +++ b/packages/client/@types/window.d.ts @@ -0,0 +1,6 @@ +declare global { + interface Window { + __misskey_input_ref__?: HTMLInputElement | null; + } +} +export type {}; diff --git a/packages/client/src/components/MkDialog.vue b/packages/client/src/components/MkDialog.vue index c1f8f581a7..a345f7dcea 100644 --- a/packages/client/src/components/MkDialog.vue +++ b/packages/client/src/components/MkDialog.vue @@ -216,25 +216,32 @@ interface Input { | "paragraph"; placeholder?: string | null; autocomplete?: string; - default: string | number | null; + default?: string | number | null; minLength?: number; maxLength?: number; } -interface Select { - items: { - value: string; - text: string; - }[]; - groupedItems: { - label: string; - items: { - value: string; - text: string; - }[]; - }[]; - default: string | null; -} +type Select = { + default?: string | null; +} & ( + | { + items: { + value: string; + text: string; + }[]; + groupedItems?: undefined; + } + | { + items?: undefined; + groupedItems: { + label: string; + items: { + value: string; + text: string; + }[]; + }[]; + } +); const props = withDefaults( defineProps<{ diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue index c778f0f6f1..e92305d8f3 100644 --- a/packages/client/src/components/MkMenu.vue +++ b/packages/client/src/components/MkMenu.vue @@ -254,8 +254,7 @@ const isActive = ref(); watch( () => props.items, () => { - // FIXME: what's this? - const items: (MenuItem | MenuPending)[] = [...props.items].filter( + const items: (MenuItem | MenuPending)[] = props.items.filter( (item) => item !== undefined, ); diff --git a/packages/client/src/components/MkUrlPreview.vue b/packages/client/src/components/MkUrlPreview.vue index e72bdd704e..95e46ee2cc 100644 --- a/packages/client/src/components/MkUrlPreview.vue +++ b/packages/client/src/components/MkUrlPreview.vue @@ -181,10 +181,10 @@ function adjustTweetHeight(message: any) { if (height) tweetHeight.value = height; } -(window as any).addEventListener("message", adjustTweetHeight); +window.addEventListener("message", adjustTweetHeight); onUnmounted(() => { - (window as any).removeEventListener("message", adjustTweetHeight); + window.removeEventListener("message", adjustTweetHeight); }); </script> diff --git a/packages/client/src/components/MkWaitingDialog.vue b/packages/client/src/components/MkWaitingDialog.vue index c35023adcb..18cec42a49 100644 --- a/packages/client/src/components/MkWaitingDialog.vue +++ b/packages/client/src/components/MkWaitingDialog.vue @@ -13,7 +13,7 @@ ]" > <i - v-if="success" + v-if="unref(success)" :class="[$style.icon, $style.success, iconify('ph-check')]" ></i> <MkLoading @@ -29,15 +29,15 @@ </template> <script lang="ts" setup> -import { shallowRef, watch } from "vue"; +import { MaybeRef, shallowRef, watch, unref } from "vue"; import MkModal from "@/components/MkModal.vue"; import iconify from "@/scripts/icon"; const modal = shallowRef<InstanceType<typeof MkModal>>(); const props = defineProps<{ - success: boolean; - showing: boolean; + success: MaybeRef<boolean>; + showing: MaybeRef<boolean>; text?: string; }>(); diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index b7a17097e3..0bdacd3adb 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -1,7 +1,7 @@ // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する import { EventEmitter } from "eventemitter3"; -import { type entities, api as firefishApi } from "firefish-js"; +import { type entities, api as firefishApi, type Endpoints } from "firefish-js"; import insertTextAtCursor from "insert-text-at-cursor"; import type { Component, Ref } from "vue"; import { defineAsyncComponent, markRaw, ref } from "vue"; @@ -13,7 +13,7 @@ import MkWaitingDialog from "@/components/MkWaitingDialog.vue"; import { apiUrl, url } from "@/config"; import { me } from "@/me"; import type { MenuItem } from "@/types/menu"; -import type { FormItemType, GetFormResultType } from "@/types/form"; +import type { Form, GetFormResultType } from "@/types/form"; export const pendingApiRequestsCount = ref(0); @@ -52,7 +52,7 @@ export const api = (( if (res.status === 200) { resolve(body); } else if (res.status === 204) { - resolve(); + resolve(undefined); } else { reject(body.error); } @@ -67,7 +67,7 @@ export const api = (( export const apiGet = (( endpoint: string, - data: Record<string, any> = {}, + data: URLSearchParams | string | string[][] | Record<string, string> = {}, token?: string | null | undefined, ) => { pendingApiRequestsCount.value++; @@ -97,7 +97,7 @@ export const apiGet = (( if (res.status === 200) { resolve(body); } else if (res.status === 204) { - resolve(); + resolve(undefined); } else { reject(body.error); } @@ -111,15 +111,15 @@ export const apiGet = (( }) as typeof apiClient.request; export const apiWithDialog = (( - endpoint: string, - data: Record<string, any> = {}, + endpoint: keyof Endpoints, + data: Record<string, unknown> = {}, token?: string | null | undefined, ) => { const promise = api(endpoint, data, token); promiseDialog(promise, null, (err) => { alert({ type: "error", - text: err.message + "\n" + (err as any).id, + text: `${err.message}\n${err.id}`, }); }); @@ -129,7 +129,7 @@ export const apiWithDialog = (( export function promiseDialog<T>( promise: Promise<T>, onSuccess?: ((res: T) => void) | null, - onFailure?: ((err: Error) => void) | null, + onFailure?: ((err: firefishApi.APIError) => void) | null, text?: string, ): Promise<T> { const showing = ref(true); @@ -294,7 +294,7 @@ export function alert(props: { text?: string | null; isPlaintext?: boolean; }): Promise<void> { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { if (props.text == null && props.type === "error") { props.text = i18n.ts.somethingHappened; } @@ -319,7 +319,7 @@ export function confirm(props: { cancelText?: string; isPlaintext?: boolean; }): Promise<{ canceled: boolean }> { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { popup( MkDialog, { @@ -342,7 +342,7 @@ export function yesno(props: { text?: string | null; isPlaintext?: boolean; }): Promise<{ canceled: boolean }> { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { popup( defineAsyncComponent({ loader: () => import("@/components/MkDialog.vue"), @@ -374,13 +374,13 @@ export function inputText(props: { minLength?: number; maxLength?: number; }): Promise< - | { canceled: true; result: undefined } + | { canceled: true; result?: undefined } | { canceled: false; result: string; } > { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { popup( MkDialog, { @@ -397,7 +397,14 @@ export function inputText(props: { }, { done: (result) => { - resolve(result || { canceled: true }); + if (result.canceled) { + resolve({ canceled: true }); + } else { + resolve({ + canceled: false, + result: String(result.result), + }); + } }, }, "closed", @@ -411,13 +418,13 @@ export function inputParagraph(props: { placeholder?: string | null; default?: string | null; }): Promise< - | { canceled: true; result: undefined } + | { canceled: true; result?: undefined } | { canceled: false; result: string; } > { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { popup( defineAsyncComponent({ loader: () => import("@/components/MkDialog.vue"), @@ -435,7 +442,14 @@ export function inputParagraph(props: { }, { done: (result) => { - resolve(result || { canceled: true }); + if (result.canceled) { + resolve({ canceled: true }); + } else { + resolve({ + canceled: false, + result: String(result.result), + }); + } }, }, "closed", @@ -450,13 +464,13 @@ export function inputNumber(props: { default?: number | null; autocomplete?: string; }): Promise< - | { canceled: true; result: undefined } + | { canceled: true; result?: undefined } | { canceled: false; result: number; } > { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { popup( defineAsyncComponent({ loader: () => import("@/components/MkDialog.vue"), @@ -475,7 +489,14 @@ export function inputNumber(props: { }, { done: (result) => { - resolve(result || { canceled: true }); + if (result.canceled) { + resolve({ canceled: true }); + } else { + resolve({ + canceled: false, + result: Number(result.result), + }); + } }, }, "closed", @@ -487,15 +508,16 @@ export function inputDate(props: { title?: string | null; text?: string | null; placeholder?: string | null; - default?: Date | null; + default?: Date | string | null; }): Promise< - | { canceled: true; result: undefined } + | { canceled: true; result?: undefined } | { canceled: false; result: Date; } > { - return new Promise((resolve, reject) => { + props.default ??= new Date(); + return new Promise((resolve, _reject) => { popup( MkDialog, { @@ -504,7 +526,10 @@ export function inputDate(props: { input: { type: "date", placeholder: props.placeholder, - default: props.default, + default: + props.default instanceof Date + ? props.default.toISOString().slice(0, 10) + : props.default, }, }, { @@ -512,7 +537,7 @@ export function inputDate(props: { resolve( result ? { - result: new Date(result.result), + result: new Date(result.result as string | number), canceled: false, } : { canceled: true }, @@ -524,7 +549,7 @@ export function inputDate(props: { }); } -export function select<C = any>( +export function select<C extends string>( props: { title?: string | null; text?: string | null; @@ -535,8 +560,10 @@ export function select<C = any>( value: C; text: string; }[]; + groupedItems?: undefined; } | { + items?: undefined; groupedItems: { label: string; items: { @@ -547,27 +574,35 @@ export function select<C = any>( } ), ): Promise< - | { canceled: true; result: undefined } + | { canceled: true; result?: undefined } | { canceled: false; result: C; } > { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { popup( MkDialog, { title: props.title, text: props.text, - select: { - items: props.items, - groupedItems: props.groupedItems, - default: props.default, - }, + select: props.items + ? { + items: props.items, + default: props.default, + } + : { + groupedItems: props.groupedItems, + default: props.default, + }, }, { done: (result) => { - resolve(result || { canceled: true }); + if (result.canceled) { + resolve({ canceled: true }); + } else { + resolve(result as never); + } }, }, "closed", @@ -576,7 +611,7 @@ export function select<C = any>( } export function success(): Promise<void> { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { const showing = ref(true); window.setTimeout(() => { showing.value = false; @@ -596,7 +631,7 @@ export function success(): Promise<void> { } export function waiting(): Promise<void> { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { const showing = ref(true); popup( MkWaitingDialog, @@ -612,14 +647,9 @@ export function waiting(): Promise<void> { }); } -export function form<T extends Record<string, FormItemType>>( - title: string, - form: T, -) { +export function form<T extends Form>(title: string, form: T) { return new Promise<{ - result?: { - [K in keyof T]: GetFormResultType<T[K]["type"]>; - }; + result?: GetFormResultType<T>; canceled?: true; }>((resolve, _reject) => { popup( @@ -639,8 +669,8 @@ export function form<T extends Record<string, FormItemType>>( }); } -export async function selectUser() { - return new Promise<entities.UserDetailed>((resolve, reject) => { +export function selectUser() { + return new Promise<entities.UserDetailed>((resolve, _reject) => { popup( defineAsyncComponent({ loader: () => import("@/components/MkUserSelectDialog.vue"), @@ -658,8 +688,8 @@ export async function selectUser() { }); } -export async function selectLocalUser() { - return new Promise((resolve, reject) => { +export function selectLocalUser() { + return new Promise<entities.UserDetailed>((resolve, _reject) => { popup( defineAsyncComponent({ loader: () => import("@/components/MkUserSelectLocalDialog.vue"), @@ -677,8 +707,8 @@ export async function selectLocalUser() { }); } -export async function selectInstance(): Promise<entities.Instance> { - return new Promise((resolve, reject) => { +export function selectInstance(): Promise<entities.Instance> { + return new Promise((resolve, _reject) => { popup( defineAsyncComponent({ loader: () => import("@/components/MkInstanceSelectDialog.vue"), @@ -696,8 +726,10 @@ export async function selectInstance(): Promise<entities.Instance> { }); } -export async function selectDriveFile(multiple: boolean) { - return new Promise((resolve, reject) => { +export function selectDriveFile<Multiple extends boolean>(multiple: Multiple) { + return new Promise< + Multiple extends true ? entities.DriveFile[] : entities.DriveFile + >((resolve, _reject) => { popup( defineAsyncComponent({ loader: () => import("@/components/MkDriveSelectDialog.vue"), @@ -711,7 +743,7 @@ export async function selectDriveFile(multiple: boolean) { { done: (files) => { if (files) { - resolve(multiple ? files : files[0]); + resolve((multiple ? files : files[0]) as never); } }, }, @@ -721,7 +753,7 @@ export async function selectDriveFile(multiple: boolean) { } export async function selectDriveFolder(multiple: boolean) { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { popup( defineAsyncComponent({ loader: () => import("@/components/MkDriveSelectDialog.vue"), @@ -745,7 +777,7 @@ export async function selectDriveFolder(multiple: boolean) { } export async function pickEmoji(src: HTMLElement | null, opts) { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { popup( defineAsyncComponent({ loader: () => import("@/components/MkEmojiPickerDialog.vue"), @@ -772,7 +804,7 @@ export async function cropImage( aspectRatio: number; }, ): Promise<entities.DriveFile> { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { popup( defineAsyncComponent({ loader: () => import("@/components/MkCropperDialog.vue"), @@ -795,13 +827,13 @@ export async function cropImage( type AwaitType<T> = T extends Promise<infer U> ? U - : T extends (...args: any[]) => Promise<infer V> + : T extends (...args: unknown[]) => Promise<infer V> ? V : T; let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null; -let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null; +let activeTextarea: HTMLTextAreaElement | HTMLInputElement; export async function openEmojiPicker( - src?: HTMLElement, + src: HTMLElement | undefined, opts, initialTextarea: typeof activeTextarea, ) { @@ -809,7 +841,7 @@ export async function openEmojiPicker( activeTextarea = initialTextarea; - const textareas = document.querySelectorAll("textarea, input"); + const textareas = document.querySelectorAll<HTMLTextAreaElement | HTMLInputElement>("textarea, input"); for (const textarea of Array.from(textareas)) { textarea.addEventListener("focus", () => { activeTextarea = textarea; @@ -821,7 +853,7 @@ export async function openEmojiPicker( for (const node of Array.from(record.addedNodes).filter( (node) => node instanceof HTMLElement, ) as HTMLElement[]) { - const textareas = node.querySelectorAll("textarea, input"); + const textareas = node.querySelectorAll<HTMLTextAreaElement | HTMLInputElement>("textarea, input"); for (const textarea of Array.from(textareas).filter( (textarea) => textarea.dataset.preventEmojiInsert == null, )) { @@ -859,7 +891,7 @@ export async function openEmojiPicker( insertTextAtCursor(activeTextarea, emoji); }, closed: () => { - openingEmojiPicker!.dispose(); + openingEmojiPicker?.dispose(); openingEmojiPicker = null; observer.disconnect(); }, @@ -910,8 +942,8 @@ export function contextMenu( ev: MouseEvent, ) { ev.preventDefault(); - return new Promise((resolve, reject) => { - let dispose; + return new Promise<void>((resolve, _reject) => { + let dispose: () => void; popup( defineAsyncComponent({ loader: () => import("@/components/MkContextMenu.vue"), @@ -938,7 +970,7 @@ export function post( props: InstanceType<typeof MkPostFormDialog>["$props"] = {}, onClosed?: () => void, ) { - return new Promise<void>((resolve, reject) => { + return new Promise<void>((resolve, _reject) => { // NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない // NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、 // Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、 @@ -966,7 +998,7 @@ export const deckGlobalEvents = new EventEmitter(); /* export function checkExistence(fileData: ArrayBuffer): Promise<any> { - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { const data = new FormData(); data.append('md5', getMD5(fileData)); diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.image.vue b/packages/client/src/pages/page-editor/els/page-editor.el.image.vue index 859f0f1112..3820b140df 100644 --- a/packages/client/src/pages/page-editor/els/page-editor.el.image.vue +++ b/packages/client/src/pages/page-editor/els/page-editor.el.image.vue @@ -44,9 +44,9 @@ const props = withDefaults( const file = ref<any>(null); async function choose() { - os.selectDriveFile(false).then((fileResponse: any) => { + os.selectDriveFile(false).then((fileResponse) => { file.value = fileResponse; - props.value.fileId = fileResponse.id; + props.value.fileId = fileResponse?.id; }); } diff --git a/packages/client/src/pages/settings/preferences-backups.vue b/packages/client/src/pages/settings/preferences-backups.vue index 202de0a082..73e098c4ea 100644 --- a/packages/client/src/pages/settings/preferences-backups.vue +++ b/packages/client/src/pages/settings/preferences-backups.vue @@ -299,12 +299,12 @@ function loadFile(): void { }); // 一応廃棄 - (window as any).__misskey_input_ref__ = null; + window.__misskey_input_ref__ = null; }; // https://qiita.com/fukasawah/items/b9dc732d95d99551013d // iOS Safari で正常に動かす為のおまじない - (window as any).__misskey_input_ref__ = input; + window.__misskey_input_ref__ = input; input.click(); } diff --git a/packages/client/src/pages/user-list-timeline.vue b/packages/client/src/pages/user-list-timeline.vue index 69f17c1047..5543f768c9 100644 --- a/packages/client/src/pages/user-list-timeline.vue +++ b/packages/client/src/pages/user-list-timeline.vue @@ -56,8 +56,8 @@ async function timetravel() { title: i18n.ts.date, }); if (canceled) return; - - tlEl.value.timetravel(date); + // FIXME: + tlEl.value!.timetravel(date); } const headerActions = computed(() => diff --git a/packages/client/src/scripts/form.ts b/packages/client/src/scripts/form.ts index 2cd0ca01dc..dfc1c646dc 100644 --- a/packages/client/src/scripts/form.ts +++ b/packages/client/src/scripts/form.ts @@ -1,72 +1,11 @@ -export type FormItem = - | { - label?: string; - type: "string"; - default: string | null; - hidden?: boolean; - multiline?: boolean; - } - | { - label?: string; - type: "number"; - default: number | null; - hidden?: boolean; - step?: number; - } - | { - label?: string; - type: "boolean"; - default: boolean | null; - hidden?: boolean; - } - | { - label?: string; - type: "enum"; - default: string | null; - hidden?: boolean; - enum: string[]; - } - | { - label?: string; - type: "radio"; - default: unknown | null; - hidden?: boolean; - options: { - label: string; - value: unknown; - }[]; - } - | { - label?: string; - type: "object"; - default: Record<string, unknown> | null; - hidden: true; - } - | { - label?: string; - type: "array"; - default: unknown[] | null; - hidden: true; - }; +// TODO: replace this file with @/types/form.ts + +import type { FormItemType, GetFormItemResultType } from "@/types/form"; + +export type FormItem = FormItemType; export type Form = Record<string, FormItem>; -type GetItemType<Item extends FormItem> = Item["type"] extends "string" - ? string - : Item["type"] extends "number" - ? number - : Item["type"] extends "boolean" - ? boolean - : Item["type"] extends "radio" - ? unknown - : Item["type"] extends "enum" - ? string - : Item["type"] extends "array" - ? unknown[] - : Item["type"] extends "object" - ? Record<string, unknown> - : never; - export type GetFormResultType<F extends Form> = { - [P in keyof F]: GetItemType<F[P]>; + [P in keyof F]: NonNullable<GetFormItemResultType<F[P]["type"]>>; }; diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 873b32d7d7..25091f2da2 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -14,6 +14,7 @@ import icon from "@/scripts/icon"; import { useRouter } from "@/router"; import { notePage } from "@/filters/note"; import type { NoteTranslation } from "@/types/note"; +import type { MenuItem } from "@/types/menu"; const router = useRouter(); @@ -291,7 +292,7 @@ export function getNoteMenu(props: { props.translating.value = false; } - let menu; + let menu: MenuItem[]; if (isSignedIn) { const statePromise = os.api("notes/state", { noteId: appearNote.id, @@ -396,7 +397,7 @@ export function getNoteMenu(props: { } : undefined, { - type: "parent", + type: "parent" as const, icon: `${icon("ph-share-network")}`, text: i18n.ts.share, children: [ @@ -499,7 +500,7 @@ export function getNoteMenu(props: { !isAppearAuthor ? null : undefined, !isAppearAuthor ? { - type: "parent", + type: "parent" as const, icon: `${icon("ph-user")}`, text: i18n.ts.user, children: getUserMenu(appearNote.user), diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts index adf9f0956a..6b51e5fef9 100644 --- a/packages/client/src/scripts/select-file.ts +++ b/packages/client/src/scripts/select-file.ts @@ -9,12 +9,14 @@ import icon from "@/scripts/icon"; const stream = useStream(); -function select( - src: any, +function select<Multiple extends boolean>( + src: HTMLElement | null | undefined, label: string | null, - multiple: boolean, -): Promise<entities.DriveFile | entities.DriveFile[]> { - return new Promise((res, rej) => { + multiple: Multiple, +) { + return new Promise< + Multiple extends true ? entities.DriveFile[] : entities.DriveFile + >((res, rej) => { const keepOriginal = ref(defaultStore.state.keepOriginalUploading); const chooseFileFromPc = () => { @@ -22,6 +24,9 @@ function select( input.type = "file"; input.multiple = multiple; input.onchange = () => { + if (input.files === null) { + return; + } const promises = Array.from(input.files).map((file) => uploadFile( file, @@ -33,19 +38,19 @@ function select( Promise.all(promises) .then((driveFiles) => { - res(multiple ? driveFiles : driveFiles[0]); + res((multiple ? driveFiles : driveFiles[0]) as never); }) .catch((err) => { // アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない }); // 一応廃棄 - (window as any).__misskey_input_ref__ = null; + window.__misskey_input_ref__ = null; }; // https://qiita.com/fukasawah/items/b9dc732d95d99551013d // iOS Safari で正常に動かす為のおまじない - (window as any).__misskey_input_ref__ = input; + window.__misskey_input_ref__ = input; input.click(); }; @@ -69,7 +74,7 @@ function select( const connection = stream.useChannel("main"); connection.on("urlUploadFinished", (urlResponse) => { if (urlResponse.marker === marker) { - res(multiple ? [urlResponse.file] : urlResponse.file); + res((multiple ? [urlResponse.file] : urlResponse.file) as never); connection.dispose(); } }); @@ -122,15 +127,15 @@ function select( } export function selectFile( - src: any, + src: HTMLElement | null | undefined, label: string | null = null, -): Promise<entities.DriveFile> { - return select(src, label, false) as Promise<entities.DriveFile>; +) { + return select(src, label, false); } export function selectFiles( - src: any, + src: HTMLElement | null | undefined, label: string | null = null, -): Promise<entities.DriveFile[]> { - return select(src, label, true) as Promise<entities.DriveFile[]>; +) { + return select(src, label, true); } diff --git a/packages/client/src/types/form.ts b/packages/client/src/types/form.ts index 9c34df30dc..88f90ebbef 100644 --- a/packages/client/src/types/form.ts +++ b/packages/client/src/types/form.ts @@ -54,18 +54,18 @@ export type FormItemSwitch = BaseFormItem & { }; export type FormItemSelect = BaseFormItem & { type: "enum"; - default?: string | null; + default?: string | number | symbol | null; enum: { - value: string | number | symbol | undefined; + value: string | number | symbol; label: string; }[]; }; export type FormItemRadios = BaseFormItem & { type: "radio"; - default?: string | number | symbol | undefined | null; + default?: string | number | symbol | null; options: { label: string; - value: string | number | symbol | undefined; + value: string | number | symbol; }[]; }; export type FormItemRange = BaseFormItem & { @@ -114,10 +114,25 @@ export type FormItemInput = FormItemInputArray[number]; export type FormItemType = FormItemTypeArray[number]; -export type GetFormItemByType<T extends FormItemType["type"], F extends FormItemType = FormItemType> = F extends {type: T} ? F : never; +export type Form = Record<string, FormItemType>; + +export type GetFormItemByType< + T extends FormItemType["type"], + F extends FormItemType = FormItemType, +> = F extends { type: T } ? F : never; type NonUndefindAble<T> = T extends undefined ? never : T; +type NonNullAble<T> = T extends null ? never : T; -export type GetFormResultType<T extends FormItemType["type"], I extends FormItemType = GetFormItemByType<T>> = NonUndefindAble< +export type GetFormItemResultType< + T extends FormItemType["type"], + I extends FormItemType = GetFormItemByType<T>, +> = NonUndefindAble< "__result_typedef" extends keyof I ? I["__result_typedef"] : I["default"] -> +>; + +export type GetFormResultType<F extends Form> = { + [K in keyof F]: F[K]["required"] extends false + ? GetFormItemResultType<F[K]["type"]> + : NonNullAble<GetFormItemResultType<F[K]["type"]>>; +}; diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts index 9f33cecfa2..1fdcca86f1 100644 --- a/packages/client/src/widgets/widget.ts +++ b/packages/client/src/widgets/widget.ts @@ -23,9 +23,7 @@ export interface WidgetComponentExpose { configure: () => void; } -export const useWidgetPropsManager = < - F extends Form & Record<string, { default: any }>, ->( +export const useWidgetPropsManager = <F extends Form>( name: string, propsDef: F, props: Readonly<WidgetComponentProps<GetFormResultType<F>>>, diff --git a/packages/firefish-js/src/api.ts b/packages/firefish-js/src/api.ts index 2639712a3f..4a7e1f0b64 100644 --- a/packages/firefish-js/src/api.ts +++ b/packages/firefish-js/src/api.ts @@ -122,6 +122,6 @@ export class APIClient { .catch(reject); }); - return promise as any; + return promise; } } diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts index 30ae1f9cdf..c0a7c9019b 100644 --- a/packages/firefish-js/src/api.types.ts +++ b/packages/firefish-js/src/api.types.ts @@ -36,6 +36,7 @@ import type { UserDetailed, UserGroup, UserList, + UserLite, UserSorting, } from "./entities"; From 8a62bf90f59d18a268832c87e61c0994ee1f2c6e Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Sun, 14 Apr 2024 03:42:30 +0800 Subject: [PATCH 052/110] fix: MkMenu.vue --- packages/client/src/components/MkMenu.vue | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue index e92305d8f3..1391024264 100644 --- a/packages/client/src/components/MkMenu.vue +++ b/packages/client/src/components/MkMenu.vue @@ -202,12 +202,12 @@ <script lang="ts" setup> import { + type Ref, defineAsyncComponent, onBeforeUnmount, onMounted, ref, watch, - shallowRef, } from "vue"; import { FocusTrap } from "focus-trap-vue"; import FormSwitch from "@/components/form/switch.vue"; @@ -242,7 +242,12 @@ const emit = defineEmits<{ const itemsEl = ref<HTMLDivElement>(); -const items2 = shallowRef<InnerMenuItem[]>([]); +/** + * Strictly speaking, this type conversion is wrong + * because `ref` will deeply unpack the `ref` in `MenuSwitch`. + * But it performs correctly, so who cares? + */ +const items2 = ref([]) as Ref<InnerMenuItem[]>; const child = ref<InstanceType<typeof XChild>>(); From 19b45866c804ab0667b26acfa7a84c5c1151874c Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 14 Apr 2024 13:35:35 +0900 Subject: [PATCH 053/110] docs: update packages/README.md --- packages/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/README.md b/packages/README.md index 56356ec2d0..6637462910 100644 --- a/packages/README.md +++ b/packages/README.md @@ -4,6 +4,7 @@ This directory contains all of the packages Firefish uses. - `backend`: Main backend code written in TypeScript for NodeJS - `backend-rs`: Backend code written in Rust, bound to NodeJS by [NAPI-RS](https://napi.rs/) +- `macro-rs`: Procedural macros for backend-rs - `client`: Web interface written in Vue3 and TypeScript - `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript - `firefish-js`: TypeScript SDK for both backend and client From baa57d7c173433f4b30f949fd1246960afebb7a0 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 14 Apr 2024 13:38:44 +0900 Subject: [PATCH 054/110] dev: add rust-analyzer to recommended VSCode extensitons --- .vscode/extensions.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 0083604d44..de0385be2a 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -12,6 +12,7 @@ "esbenp.prettier-vscode", "redhat.vscode-yaml", "yoavbls.pretty-ts-errors", - "biomejs.biome" + "biomejs.biome", + "rust-lang.rust-analyzer" ] } From 70aa3704ef27932ef0d3da543f169c0099f103bb Mon Sep 17 00:00:00 2001 From: sup39 <dev@sup39.dev> Date: Sun, 14 Apr 2024 14:41:01 +0900 Subject: [PATCH 055/110] refactor (backend): port password hashing/verification to backend-rs Co-authored-by: naskya <m@naskya.net> --- Cargo.lock | 92 ++++++++++++++++++- Cargo.toml | 2 + packages/backend-rs/Cargo.toml | 2 + packages/backend-rs/index.d.ts | 3 + packages/backend-rs/index.js | 5 +- packages/backend-rs/src/misc/mod.rs | 1 + packages/backend-rs/src/misc/password.rs | 69 ++++++++++++++ packages/backend/package.json | 3 - packages/backend/src/misc/password.ts | 20 ---- .../backend/src/server/api/common/signup.ts | 5 +- .../api/endpoints/admin/reset-password.ts | 6 +- .../server/api/endpoints/i/2fa/key-done.ts | 6 +- .../api/endpoints/i/2fa/register-key.ts | 7 +- .../server/api/endpoints/i/2fa/register.ts | 6 +- .../server/api/endpoints/i/2fa/remove-key.ts | 6 +- .../server/api/endpoints/i/2fa/unregister.ts | 6 +- .../server/api/endpoints/i/change-password.ts | 6 +- .../server/api/endpoints/i/delete-account.ts | 6 +- .../api/endpoints/i/regenerate-token.ts | 6 +- .../server/api/endpoints/i/update-email.ts | 6 +- .../server/api/endpoints/reset-password.ts | 4 +- .../backend/src/server/api/private/signin.ts | 16 ++-- .../backend/src/server/api/private/signup.ts | 6 +- .../src/services/create-system-user.ts | 5 +- pnpm-lock.yaml | 38 +------- 25 files changed, 214 insertions(+), 118 deletions(-) create mode 100644 packages/backend-rs/src/misc/password.rs delete mode 100644 packages/backend/src/misc/password.ts diff --git a/Cargo.lock b/Cargo.lock index 93c475fa0b..6552351a3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,6 +132,18 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -190,8 +202,10 @@ checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" name = "backend-rs" version = "0.0.0" dependencies = [ + "argon2", "async-trait", "basen", + "bcrypt", "cfg-if", "chrono", "cuid2", @@ -238,6 +252,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "base64ct" version = "1.6.0" @@ -250,6 +270,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dbe4bb73fd931c4d1aaf53b35d1286c8a948ad00ec92c8e3c856f15fd027f43" +[[package]] +name = "bcrypt" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" +dependencies = [ + "base64 0.22.0", + "blowfish", + "getrandom", + "subtle", + "zeroize", +] + [[package]] name = "bigdecimal" version = "0.3.1" @@ -303,6 +336,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -312,6 +354,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "borsh" version = "1.4.0" @@ -415,6 +467,16 @@ dependencies = [ "windows-targets 0.52.4", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.4" @@ -1075,6 +1137,15 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -1122,7 +1193,7 @@ checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" dependencies = [ "ahash 0.8.11", "anyhow", - "base64", + "base64 0.21.7", "bytecount", "clap", "fancy-regex", @@ -1559,6 +1630,17 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" @@ -1801,7 +1883,7 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -1947,7 +2029,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", ] [[package]] @@ -2398,7 +2480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", - "base64", + "base64 0.21.7", "bigdecimal", "bitflags 2.5.0", "byteorder", @@ -2445,7 +2527,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", - "base64", + "base64 0.21.7", "bigdecimal", "bitflags 2.5.0", "byteorder", diff --git a/Cargo.toml b/Cargo.toml index f7ba12d884..1a72cb2c5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,10 @@ napi = { version = "2.16.2", default-features = false } napi-derive = "2.16.2" napi-build = "2.1.2" +argon2 = "0.5.3" async-trait = "0.1.80" basen = "0.1.0" +bcrypt = "0.15.1" cfg-if = "1.0.0" chrono = "0.4.37" convert_case = "0.6.0" diff --git a/packages/backend-rs/Cargo.toml b/packages/backend-rs/Cargo.toml index 783f630942..235fcc8706 100644 --- a/packages/backend-rs/Cargo.toml +++ b/packages/backend-rs/Cargo.toml @@ -17,8 +17,10 @@ macro_rs = { workspace = true } napi = { workspace = true, optional = true, default-features = false, features = ["napi9", "tokio_rt", "chrono_date", "serde-json"] } napi-derive = { workspace = true, optional = true } +argon2 = { workspace = true, features = ["std"] } async-trait = { workspace = true } basen = { workspace = true } +bcrypt = { workspace = true } cfg-if = { workspace = true } chrono = { workspace = true } cuid2 = { workspace = true } diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 617bedf5b9..f2830dcac3 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -135,6 +135,9 @@ export function toPuny(host: string): string export function toMastodonId(firefishId: string): string | null export function fromMastodonId(mastodonId: string): string | null export function nyaify(text: string, lang?: string | undefined | null): string +export function hashPassword(password: string): string +export function verifyPassword(password: string, hash: string): boolean +export function isOldPasswordAlgorithm(hash: string): boolean export interface AbuseUserReport { id: string createdAt: Date diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index f2942a9aa5..af9a293bbb 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, toMastodonId, fromMastodonId, nyaify, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, toMastodonId, fromMastodonId, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding module.exports.readServerConfig = readServerConfig module.exports.stringToAcct = stringToAcct @@ -324,6 +324,9 @@ module.exports.toPuny = toPuny module.exports.toMastodonId = toMastodonId module.exports.fromMastodonId = fromMastodonId module.exports.nyaify = nyaify +module.exports.hashPassword = hashPassword +module.exports.verifyPassword = verifyPassword +module.exports.isOldPasswordAlgorithm = isOldPasswordAlgorithm module.exports.AntennaSrcEnum = AntennaSrcEnum module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum module.exports.NoteVisibilityEnum = NoteVisibilityEnum diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index 701e35a4eb..6eda4e449e 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -3,3 +3,4 @@ pub mod check_word_mute; pub mod convert_host; pub mod mastodon_id; pub mod nyaify; +pub mod password; diff --git a/packages/backend-rs/src/misc/password.rs b/packages/backend-rs/src/misc/password.rs new file mode 100644 index 0000000000..b21ff73499 --- /dev/null +++ b/packages/backend-rs/src/misc/password.rs @@ -0,0 +1,69 @@ +use argon2::{ + password_hash, + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; + +#[crate::export] +pub fn hash_password(password: &str) -> Result<String, password_hash::errors::Error> { + let salt = SaltString::generate(&mut OsRng); + Ok(Argon2::default() + .hash_password(password.as_bytes(), &salt)? + .to_string()) +} + +#[derive(thiserror::Error, Debug)] +pub enum VerifyError { + #[error("An error occured while bcrypt verification: {0}")] + BcryptError(#[from] bcrypt::BcryptError), + #[error("Invalid argon2 password hash: {0}")] + InvalidArgon2Hash(#[from] password_hash::Error), + #[error("An error occured while argon2 verification: {0}")] + Argon2Error(#[from] argon2::Error), +} + +#[crate::export] +pub fn verify_password(password: &str, hash: &str) -> Result<bool, VerifyError> { + if is_old_password_algorithm(hash) { + Ok(bcrypt::verify(password, hash)?) + } else { + let parsed_hash = PasswordHash::new(hash)?; + Ok(Argon2::default() + .verify_password(password.as_bytes(), &parsed_hash) + .is_ok()) + } +} + +#[inline] +#[crate::export] +pub fn is_old_password_algorithm(hash: &str) -> bool { + // bcrypt hashes start with $2[ab]$ + hash.starts_with("$2") +} + +#[cfg(test)] +mod unit_test { + use super::{hash_password, is_old_password_algorithm, verify_password}; + + #[test] + fn verify_password_test() { + let password = "omWc*%sD^fn7o2cXmc9e2QasBdrbRuhNB*gx!J5"; + + let hash = hash_password(password).unwrap(); + assert!(verify_password(password, hash.as_str()).unwrap()); + + let argon2_hash = "$argon2id$v=19$m=19456,t=2,p=1$jty3puDFd4ENv/lgHn3ROQ$kRHDdEoVv2rruvnF731E74NxnYlvj5FMgePdGIIq3Jk"; + let argon2_invalid_hash = "$argon2id$v=19$m=19456,t=2,p=1$jty3puDFd4ENv/lgHn3ROQ$kRHDdEoVv2rruvnF731E74NxnYlvj4FMgePdGIIq3Jk"; + let bcrypt_hash = "$2a$12$WzUc.20jgbHmQjUMqTr8vOhKqYbS1BUvubapv/GLjCK1IN.h4e4la"; + let bcrypt_invalid_hash = "$2a$12$WzUc.20jgbHmQjUMqTr7vOhKqYbS1BUvubapv/GLjCK1IN.h4e4la"; + + assert!(!is_old_password_algorithm(argon2_hash)); + assert!(is_old_password_algorithm(bcrypt_hash)); + + assert!(verify_password(password, argon2_hash).unwrap()); + assert!(verify_password(password, bcrypt_hash).unwrap()); + + assert!(!verify_password(password, argon2_invalid_hash).unwrap()); + assert!(!verify_password(password, bcrypt_invalid_hash).unwrap()); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 5382f1d361..5dbf7cad64 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -37,11 +37,9 @@ "adm-zip": "0.5.10", "ajv": "8.12.0", "archiver": "7.0.1", - "argon2": "^0.40.1", "aws-sdk": "2.1597.0", "axios": "^1.6.8", "backend-rs": "workspace:*", - "bcryptjs": "2.4.3", "blurhash": "2.0.5", "bull": "4.12.2", "cacheable-lookup": "TheEssem/cacheable-lookup", @@ -130,7 +128,6 @@ "@swc/cli": "0.3.12", "@swc/core": "1.4.13", "@types/adm-zip": "^0.5.5", - "@types/bcryptjs": "2.4.6", "@types/color-convert": "^2.0.3", "@types/content-disposition": "^0.5.8", "@types/escape-regexp": "0.0.3", diff --git a/packages/backend/src/misc/password.ts b/packages/backend/src/misc/password.ts deleted file mode 100644 index c63f89f5c9..0000000000 --- a/packages/backend/src/misc/password.ts +++ /dev/null @@ -1,20 +0,0 @@ -import bcrypt from "bcryptjs"; -import * as argon2 from "argon2"; - -export async function hashPassword(password: string): Promise<string> { - return argon2.hash(password); -} - -export async function comparePassword( - password: string, - hash: string, -): Promise<boolean> { - if (isOldAlgorithm(hash)) return bcrypt.compare(password, hash); - - return argon2.verify(hash, password); -} - -export function isOldAlgorithm(hash: string): boolean { - // bcrypt hashes start with $2[ab]$ - return hash.startsWith("$2"); -} diff --git a/packages/backend/src/server/api/common/signup.ts b/packages/backend/src/server/api/common/signup.ts index a267baf9c0..58b88b7d02 100644 --- a/packages/backend/src/server/api/common/signup.ts +++ b/packages/backend/src/server/api/common/signup.ts @@ -4,12 +4,11 @@ import { User } from "@/models/entities/user.js"; import { Users, UsedUsernames } from "@/models/index.js"; import { UserProfile } from "@/models/entities/user-profile.js"; import { IsNull } from "typeorm"; -import { genId, toPuny } from "backend-rs"; +import { genId, hashPassword, toPuny } from "backend-rs"; import { UserKeypair } from "@/models/entities/user-keypair.js"; import { UsedUsername } from "@/models/entities/used-username.js"; import { db } from "@/db/postgre.js"; import config from "@/config/index.js"; -import { hashPassword } from "@/misc/password.js"; export async function signup(opts: { username: User["username"]; @@ -40,7 +39,7 @@ export async function signup(opts: { } // Generate hash of password - hash = await hashPassword(password); + hash = hashPassword(password); } // Generate secret diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index 5fbed130e6..ace0b581d7 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -1,8 +1,7 @@ import define from "@/server/api/define.js"; -// import bcrypt from "bcryptjs"; import rndstr from "rndstr"; import { Users, UserProfiles } from "@/models/index.js"; -import { hashPassword } from "@/misc/password.js"; +import { hashPassword } from "backend-rs"; export const meta = { tags: ["admin"], @@ -48,8 +47,7 @@ export default define(meta, paramDef, async (ps) => { const passwd = rndstr("a-zA-Z0-9", 8); // Generate hash of password - // const hash = bcrypt.hashSync(passwd); - const hash = await hashPassword(passwd); + const hash = hashPassword(passwd); await UserProfiles.update( { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index 0e52dd0d78..6c99217e7d 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -9,7 +9,7 @@ import { import config from "@/config/index.js"; import { procedures, hash } from "@/server/api/2fa.js"; import { publishMainStream } from "@/services/stream.js"; -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; const rpIdHashReal = hash(Buffer.from(config.hostname, "utf-8")); @@ -40,8 +40,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index b275b5705d..4991e8fc90 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -2,9 +2,8 @@ import define from "@/server/api/define.js"; import { UserProfiles, AttestationChallenges } from "@/models/index.js"; import { promisify } from "node:util"; import * as crypto from "node:crypto"; -import { genId } from "backend-rs"; +import { genId, verifyPassword } from "backend-rs"; import { hash } from "@/server/api/2fa.js"; -import { comparePassword } from "@/misc/password.js"; const randomBytes = promisify(crypto.randomBytes); @@ -25,8 +24,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index 52e1df39f4..c0e6137d5d 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -3,7 +3,7 @@ import * as QRCode from "qrcode"; import config from "@/config/index.js"; import { UserProfiles } from "@/models/index.js"; import define from "@/server/api/define.js"; -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; export const meta = { requireCredential: true, @@ -22,8 +22,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index 0cdf8780ef..4259d8f70d 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -1,4 +1,4 @@ -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; import define from "@/server/api/define.js"; import { UserProfiles, UserSecurityKeys, Users } from "@/models/index.js"; import { publishMainStream } from "@/services/stream.js"; @@ -21,8 +21,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index c4e78eecb5..240ff2b34e 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -1,7 +1,7 @@ import { publishMainStream } from "@/services/stream.js"; import define from "@/server/api/define.js"; import { Users, UserProfiles } from "@/models/index.js"; -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; export const meta = { requireCredential: true, @@ -20,8 +20,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts index b0dc8bba60..1634676748 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -1,6 +1,6 @@ import define from "@/server/api/define.js"; import { UserProfiles } from "@/models/index.js"; -import { hashPassword, comparePassword } from "@/misc/password.js"; +import { hashPassword, verifyPassword } from "backend-rs"; export const meta = { requireCredential: true, @@ -20,8 +20,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.currentPassword, profile.password!); + // Compare passwords + const same = verifyPassword(ps.currentPassword, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index 606cde82e1..538798261d 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -1,7 +1,7 @@ import { UserProfiles, Users } from "@/models/index.js"; import { deleteAccount } from "@/services/delete-account.js"; import define from "@/server/api/define.js"; -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; export const meta = { requireCredential: true, @@ -24,8 +24,8 @@ export default define(meta, paramDef, async (ps, user) => { return; } - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index c1b4325adb..fd3023ab7a 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -6,7 +6,7 @@ import { import generateUserToken from "@/server/api/common/generate-native-user-token.js"; import define from "@/server/api/define.js"; import { Users, UserProfiles } from "@/models/index.js"; -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; export const meta = { requireCredential: true, @@ -28,8 +28,8 @@ export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index a48252ed1a..234127f584 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -7,7 +7,7 @@ import { sendEmail } from "@/services/send-email.js"; import { ApiError } from "@/server/api/error.js"; import { validateEmailForAccount } from "@/services/validate-email-for-account.js"; import { HOUR } from "@/const.js"; -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; export const meta = { requireCredential: true, @@ -46,8 +46,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new ApiError(meta.errors.incorrectPassword); diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index ff5c8d987f..b69b1b17d3 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -1,6 +1,6 @@ import { UserProfiles, PasswordResetRequests } from "@/models/index.js"; import define from "@/server/api/define.js"; -import { hashPassword } from "@/misc/password.js"; +import { hashPassword } from "backend-rs"; export const meta = { tags: ["reset password"], @@ -32,7 +32,7 @@ export default define(meta, paramDef, async (ps, user) => { } // Generate hash of password - const hash = await hashPassword(ps.password); + const hash = hashPassword(ps.password); await UserProfiles.update(req.userId, { password: hash, diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index 8692b01ad1..a7eb623062 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -10,12 +10,12 @@ import { AttestationChallenges, } from "@/models/index.js"; import type { ILocalUser } from "@/models/entities/user.js"; -import { genId } from "backend-rs"; import { - comparePassword, + genId, hashPassword, - isOldAlgorithm, -} from "@/misc/password.js"; + isOldPasswordAlgorithm, + verifyPassword, +} from "backend-rs"; import { verifyLogin, hash } from "@/server/api/2fa.js"; import { randomBytes } from "node:crypto"; import { IsNull } from "typeorm"; @@ -91,11 +91,11 @@ export default async (ctx: Koa.Context) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(password, profile.password!); + // Compare passwords + const same = verifyPassword(password, profile.password!); - if (same && isOldAlgorithm(profile.password!)) { - profile.password = await hashPassword(password); + if (same && isOldPasswordAlgorithm(profile.password!)) { + profile.password = hashPassword(password); await UserProfiles.save(profile); } diff --git a/packages/backend/src/server/api/private/signup.ts b/packages/backend/src/server/api/private/signup.ts index 8179300884..64b5c57337 100644 --- a/packages/backend/src/server/api/private/signup.ts +++ b/packages/backend/src/server/api/private/signup.ts @@ -6,10 +6,8 @@ import { Users, RegistrationTickets, UserPendings } from "@/models/index.js"; import { signup } from "@/server/api/common/signup.js"; import config from "@/config/index.js"; import { sendEmail } from "@/services/send-email.js"; -import { genId } from "backend-rs"; +import { genId, hashPassword } from "backend-rs"; import { validateEmailForAccount } from "@/services/validate-email-for-account.js"; -import { hashPassword } from "@/misc/password.js"; -import { inspect } from "node:util"; export default async (ctx: Koa.Context) => { const body = ctx.request.body; @@ -85,7 +83,7 @@ export default async (ctx: Koa.Context) => { const code = rndstr("a-z0-9", 16); // Generate hash of password - const hash = await hashPassword(password); + const hash = hashPassword(password); await UserPendings.insert({ id: genId(), diff --git a/packages/backend/src/services/create-system-user.ts b/packages/backend/src/services/create-system-user.ts index 345de7ad17..802c59b288 100644 --- a/packages/backend/src/services/create-system-user.ts +++ b/packages/backend/src/services/create-system-user.ts @@ -4,17 +4,16 @@ import { genRsaKeyPair } from "@/misc/gen-key-pair.js"; import { User } from "@/models/entities/user.js"; import { UserProfile } from "@/models/entities/user-profile.js"; import { IsNull } from "typeorm"; -import { genId } from "backend-rs"; +import { genId, hashPassword } from "backend-rs"; import { UserKeypair } from "@/models/entities/user-keypair.js"; import { UsedUsername } from "@/models/entities/used-username.js"; import { db } from "@/db/postgre.js"; -import { hashPassword } from "@/misc/password.js"; export async function createSystemUser(username: string) { const password = uuid(); // Generate hash of password - const hash = await hashPassword(password); + const hash = hashPassword(password); // Generate secret const secret = generateNativeUserToken(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 192b5f95e5..02b473ddce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,9 +99,6 @@ importers: archiver: specifier: 7.0.1 version: 7.0.1 - argon2: - specifier: ^0.40.1 - version: 0.40.1 aws-sdk: specifier: 2.1597.0 version: 2.1597.0 @@ -111,9 +108,6 @@ importers: backend-rs: specifier: workspace:* version: link:../backend-rs - bcryptjs: - specifier: 2.4.3 - version: 2.4.3 blurhash: specifier: 2.0.5 version: 2.0.5 @@ -377,9 +371,6 @@ importers: '@types/adm-zip': specifier: ^0.5.5 version: 0.5.5 - '@types/bcryptjs': - specifier: 2.4.6 - version: 2.4.6 '@types/color-convert': specifier: ^2.0.3 version: 2.0.3 @@ -3294,11 +3285,6 @@ packages: sshpk: 1.17.0 dev: false - /@phc/format@1.0.0: - resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} - engines: {node: '>=10'} - dev: false - /@phosphor-icons/web@2.1.1: resolution: {integrity: sha512-QjrfbItu5Rb2i37GzsKxmrRHfZPTVk3oXSPBnQ2+oACDbQRWGAeB0AsvZw263n1nFouQuff+khOCtRbrc6+k+A==} dev: true @@ -3831,10 +3817,6 @@ packages: '@babel/types': 7.23.0 dev: true - /@types/bcryptjs@2.4.6: - resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} - dev: true - /@types/body-parser@1.19.2: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: @@ -5358,16 +5340,6 @@ packages: /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - /argon2@0.40.1: - resolution: {integrity: sha512-DjtHDwd7pm12qeWyfihHoM8Bn5vGcgH6sKwgPqwNYroRmxlrzadHEvMyuvQxN/V8YSyRRKD5x6ito09q1e9OyA==} - engines: {node: '>=16.17.0'} - requiresBuild: true - dependencies: - '@phc/format': 1.0.0 - node-addon-api: 7.1.0 - node-gyp-build: 4.8.0 - dev: false - /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -5893,10 +5865,6 @@ packages: tweetnacl: 0.14.5 dev: false - /bcryptjs@2.4.3: - resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} - dev: false - /bin-check@4.1.0: resolution: {integrity: sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==} engines: {node: '>=4'} @@ -13029,11 +12997,6 @@ packages: dev: true optional: true - /node-addon-api@7.1.0: - resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==} - engines: {node: ^16 || ^18 || >= 20} - dev: false - /node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -13083,6 +13046,7 @@ packages: /node-gyp-build@4.8.0: resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==} hasBin: true + dev: true /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} From ceca260c922f40318279cf2871da3a8f718ae4df Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 14 Apr 2024 14:53:25 +0900 Subject: [PATCH 056/110] refactor (backend): port convert-milliseconds to backend-rs --- packages/backend-rs/index.d.ts | 2 + packages/backend-rs/index.js | 3 +- .../src/misc/format_milliseconds.rs | 46 +++++++++++++++++++ packages/backend-rs/src/misc/mod.rs | 1 + .../backend/src/misc/convert-milliseconds.ts | 17 ------- packages/backend/src/server/api/limiter.ts | 4 +- 6 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 packages/backend-rs/src/misc/format_milliseconds.rs delete mode 100644 packages/backend/src/misc/convert-milliseconds.ts diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index f2830dcac3..d3bb13164a 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -132,6 +132,8 @@ export function isSelfHost(host?: string | undefined | null): boolean export function isSameOrigin(uri: string): boolean export function extractHost(uri: string): string export function toPuny(host: string): string +/** Convert milliseconds to a human readable string */ +export function formatMilliseconds(milliseconds: number): string export function toMastodonId(firefishId: string): string | null export function fromMastodonId(mastodonId: string): string | null export function nyaify(text: string, lang?: string | undefined | null): string diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index af9a293bbb..6fbfe9414d 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, toMastodonId, fromMastodonId, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, formatMilliseconds, toMastodonId, fromMastodonId, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding module.exports.readServerConfig = readServerConfig module.exports.stringToAcct = stringToAcct @@ -321,6 +321,7 @@ module.exports.isSelfHost = isSelfHost module.exports.isSameOrigin = isSameOrigin module.exports.extractHost = extractHost module.exports.toPuny = toPuny +module.exports.formatMilliseconds = formatMilliseconds module.exports.toMastodonId = toMastodonId module.exports.fromMastodonId = fromMastodonId module.exports.nyaify = nyaify diff --git a/packages/backend-rs/src/misc/format_milliseconds.rs b/packages/backend-rs/src/misc/format_milliseconds.rs new file mode 100644 index 0000000000..dfa8df6f62 --- /dev/null +++ b/packages/backend-rs/src/misc/format_milliseconds.rs @@ -0,0 +1,46 @@ +/// Convert milliseconds to a human readable string +#[crate::export] +pub fn format_milliseconds(milliseconds: u32) -> String { + let mut seconds = milliseconds / 1000; + let mut minutes = seconds / 60; + let mut hours = minutes / 60; + let days = hours / 24; + + seconds %= 60; + minutes %= 60; + hours %= 24; + + let mut buf: Vec<String> = vec![]; + + if days > 0 { + buf.push(format!("{} day(s)", days)); + } + if hours > 0 { + buf.push(format!("{} hour(s)", hours)); + } + if minutes > 0 { + buf.push(format!("{} minute(s)", minutes)); + } + if seconds > 0 { + buf.push(format!("{} second(s)", seconds)); + } + + buf.join(", ") +} + +#[cfg(test)] +mod unit_test { + use super::format_milliseconds; + use pretty_assertions::assert_eq; + + #[test] + fn format_milliseconds_test() { + assert_eq!(format_milliseconds(1000), "1 second(s)"); + assert_eq!(format_milliseconds(1387938), "23 minute(s), 7 second(s)"); + assert_eq!(format_milliseconds(34200457), "9 hour(s), 30 minute(s)"); + assert_eq!( + format_milliseconds(998244353), + "11 day(s), 13 hour(s), 17 minute(s), 24 second(s)" + ); + } +} diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index 6eda4e449e..d5a84e56a1 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -1,6 +1,7 @@ pub mod acct; pub mod check_word_mute; pub mod convert_host; +pub mod format_milliseconds; pub mod mastodon_id; pub mod nyaify; pub mod password; diff --git a/packages/backend/src/misc/convert-milliseconds.ts b/packages/backend/src/misc/convert-milliseconds.ts deleted file mode 100644 index d8c163ffda..0000000000 --- a/packages/backend/src/misc/convert-milliseconds.ts +++ /dev/null @@ -1,17 +0,0 @@ -export function convertMilliseconds(ms: number) { - let seconds = Math.round(ms / 1000); - let minutes = Math.round(seconds / 60); - let hours = Math.round(minutes / 60); - const days = Math.round(hours / 24); - seconds %= 60; - minutes %= 60; - hours %= 24; - - const result = []; - if (days > 0) result.push(`${days} day(s)`); - if (hours > 0) result.push(`${hours} hour(s)`); - if (minutes > 0) result.push(`${minutes} minute(s)`); - if (seconds > 0) result.push(`${seconds} second(s)`); - - return result.join(", "); -} diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts index f03f8754cf..d4a9353ade 100644 --- a/packages/backend/src/server/api/limiter.ts +++ b/packages/backend/src/server/api/limiter.ts @@ -2,7 +2,7 @@ import Limiter from "ratelimiter"; import Logger from "@/services/logger.js"; import { redisClient } from "@/db/redis.js"; import type { IEndpointMeta } from "./endpoints.js"; -import { convertMilliseconds } from "@/misc/convert-milliseconds.js"; +import { formatMilliseconds } from "backend-rs"; const logger = new Logger("limiter"); @@ -78,7 +78,7 @@ export const limiter = ( if (info.remaining === 0) { reject({ message: "RATE_LIMIT_EXCEEDED", - remainingTime: convertMilliseconds(info.resetMs - Date.now()), + remainingTime: formatMilliseconds(info.resetMs - Date.now()), }); } else { ok(); From b71da18b032d3cf6a6593e648c9fe9ac04726f29 Mon Sep 17 00:00:00 2001 From: sup39 <dev@sup39.dev> Date: Sun, 14 Apr 2024 20:16:22 +0900 Subject: [PATCH 057/110] refactor (backend): port fetch-meta to backend-rs Co-authored-by: naskya <m@naskya.net> --- packages/backend-rs/index.d.ts | 13 +++ packages/backend-rs/index.js | 4 +- packages/backend-rs/src/misc/meta.rs | 83 +++++++++++++++++++ packages/backend-rs/src/misc/mod.rs | 1 + packages/backend/src/daemons/server-stats.ts | 4 +- packages/backend/src/misc/fetch-meta.ts | 72 ---------------- .../backend/src/misc/fetch-proxy-account.ts | 4 +- packages/backend/src/misc/reaction-lib.ts | 6 +- .../backend/src/misc/should-block-instance.ts | 6 +- .../backend/src/misc/skipped-instances.ts | 4 +- packages/backend/src/misc/translate.ts | 4 +- .../backend/src/queue/processors/inbox.ts | 4 +- .../src/remote/activitypub/check-fetch.ts | 6 +- .../src/remote/activitypub/models/image.ts | 4 +- .../src/remote/activitypub/resolver.ts | 4 +- packages/backend/src/server/activitypub.ts | 21 +++-- .../src/server/activitypub/featured.ts | 4 +- .../src/server/activitypub/followers.ts | 4 +- .../src/server/activitypub/following.ts | 4 +- .../backend/src/server/activitypub/outbox.ts | 4 +- .../backend/src/server/api/api-handler.ts | 4 +- packages/backend/src/server/api/call.ts | 4 +- .../src/server/api/endpoints/admin/meta.ts | 4 +- .../src/server/api/endpoints/custom-motd.ts | 4 +- .../api/endpoints/custom-splash-icons.ts | 4 +- .../backend/src/server/api/endpoints/drive.ts | 4 +- .../api/endpoints/drive/files/create.ts | 4 +- .../api/endpoints/federation/instances.ts | 6 +- .../server/api/endpoints/hashtags/trend.ts | 4 +- .../server/api/endpoints/i/import-posts.ts | 4 +- .../backend/src/server/api/endpoints/meta.ts | 4 +- .../api/endpoints/notes/global-timeline.ts | 4 +- .../api/endpoints/notes/hybrid-timeline.ts | 4 +- .../api/endpoints/notes/local-timeline.ts | 4 +- .../endpoints/notes/recommended-timeline.ts | 4 +- .../src/server/api/endpoints/pinned-users.ts | 4 +- .../api/endpoints/recommended-instances.ts | 4 +- .../src/server/api/endpoints/server-info.ts | 4 +- .../src/server/api/endpoints/sw/register.ts | 4 +- .../api/endpoints/users/report-abuse.ts | 4 +- .../src/server/api/mastodon/endpoints/meta.ts | 4 +- .../server/api/mastodon/endpoints/status.ts | 7 +- .../backend/src/server/api/private/signup.ts | 5 +- .../api/stream/channels/global-timeline.ts | 5 +- .../api/stream/channels/hybrid-timeline.ts | 5 +- .../api/stream/channels/local-timeline.ts | 5 +- .../stream/channels/recommended-timeline.ts | 7 +- packages/backend/src/server/index.ts | 4 +- packages/backend/src/server/nodeinfo.ts | 4 +- packages/backend/src/server/web/index.ts | 21 +++-- packages/backend/src/server/web/manifest.ts | 4 +- .../backend/src/server/web/url-preview.ts | 4 +- .../backend/src/services/drive/add-file.ts | 10 +-- .../backend/src/services/drive/delete-file.ts | 4 +- .../backend/src/services/push-notification.ts | 4 +- packages/backend/src/services/send-email.ts | 4 +- .../services/validate-email-for-account.ts | 4 +- 57 files changed, 229 insertions(+), 210 deletions(-) create mode 100644 packages/backend-rs/src/misc/meta.rs delete mode 100644 packages/backend/src/misc/fetch-meta.ts diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index d3bb13164a..3770056d8f 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -136,6 +136,19 @@ export function toPuny(host: string): string export function formatMilliseconds(milliseconds: number): string export function toMastodonId(firefishId: string): string | null export function fromMastodonId(mastodonId: string): string | null +export function fetchMeta(useCache: boolean): Promise<Meta> +export interface PugArgs { + img: string | null + title: string + instanceName: string + desc: string | null + icon: string | null + splashIcon: string | null + themeColor: string | null + randomMotd: string + privateMode: boolean | null +} +export function metaToPugArgs(meta: Meta): PugArgs export function nyaify(text: string, lang?: string | undefined | null): string export function hashPassword(password: string): string export function verifyPassword(password: string, hash: string): boolean diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 6fbfe9414d..363c858e4a 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, formatMilliseconds, toMastodonId, fromMastodonId, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding module.exports.readServerConfig = readServerConfig module.exports.stringToAcct = stringToAcct @@ -324,6 +324,8 @@ module.exports.toPuny = toPuny module.exports.formatMilliseconds = formatMilliseconds module.exports.toMastodonId = toMastodonId module.exports.fromMastodonId = fromMastodonId +module.exports.fetchMeta = fetchMeta +module.exports.metaToPugArgs = metaToPugArgs module.exports.nyaify = nyaify module.exports.hashPassword = hashPassword module.exports.verifyPassword = verifyPassword diff --git a/packages/backend-rs/src/misc/meta.rs b/packages/backend-rs/src/misc/meta.rs new file mode 100644 index 0000000000..5aed617038 --- /dev/null +++ b/packages/backend-rs/src/misc/meta.rs @@ -0,0 +1,83 @@ +use crate::database::db_conn; +use crate::model::entity::meta; +use rand::prelude::*; +use sea_orm::{prelude::*, ActiveValue}; +use std::sync::Mutex; + +type Meta = meta::Model; + +static CACHE: Mutex<Option<Meta>> = Mutex::new(None); +fn update_cache(meta: &Meta) { + let _ = CACHE.lock().map(|mut cache| *cache = Some(meta.clone())); +} + +#[crate::export] +pub async fn fetch_meta(use_cache: bool) -> Result<Meta, DbErr> { + // try using cache + if use_cache { + if let Some(cache) = CACHE.lock().ok().and_then(|cache| cache.clone()) { + return Ok(cache); + } + } + + // try fetching from db + let db = db_conn().await?; + let meta = meta::Entity::find().one(db).await?; + if let Some(meta) = meta { + update_cache(&meta); + return Ok(meta); + } + + // create a new meta object and insert into db + let meta = meta::Entity::insert(meta::ActiveModel { + id: ActiveValue::Set("x".to_owned()), + ..Default::default() + }) + .exec_with_returning(db) + .await?; + update_cache(&meta); + Ok(meta) +} + +#[crate::export(object)] +pub struct PugArgs { + pub img: Option<String>, + pub title: String, + pub instance_name: String, + pub desc: Option<String>, + pub icon: Option<String>, + pub splash_icon: Option<String>, + pub theme_color: Option<String>, + pub random_motd: String, + pub private_mode: Option<bool>, +} + +#[crate::export] +pub fn meta_to_pug_args(meta: Meta) -> PugArgs { + let mut rng = rand::thread_rng(); + + let splash_icon = meta + .custom_splash_icons + .choose(&mut rng) + .map(|s| s.to_owned()) + .or_else(|| meta.icon_url.to_owned()); + + let random_motd = meta + .custom_motd + .choose(&mut rng) + .map(|s| s.to_owned()) + .unwrap_or_else(|| "Loading...".to_owned()); + + let name = meta.name.unwrap_or_else(|| "Firefish".to_owned()); + PugArgs { + img: meta.banner_url, + title: name.clone(), + instance_name: name.clone(), + desc: meta.description, + icon: meta.icon_url, + splash_icon, + theme_color: meta.theme_color, + random_motd, + private_mode: meta.private_mode, + } +} diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index d5a84e56a1..7f99a67324 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -3,5 +3,6 @@ pub mod check_word_mute; pub mod convert_host; pub mod format_milliseconds; pub mod mastodon_id; +pub mod meta; pub mod nyaify; pub mod password; diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts index 58a9b1491b..92265ecba7 100644 --- a/packages/backend/src/daemons/server-stats.ts +++ b/packages/backend/src/daemons/server-stats.ts @@ -1,7 +1,7 @@ import si from "systeminformation"; import Xev from "xev"; import * as osUtils from "os-utils"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; const ev = new Xev(); @@ -20,7 +20,7 @@ export default function () { ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50)); }); - fetchMeta().then((meta) => { + fetchMeta(true).then((meta) => { if (!meta.enableServerMachineStats) return; }); diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts deleted file mode 100644 index fdc978b5a3..0000000000 --- a/packages/backend/src/misc/fetch-meta.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Meta } from "@/models/entities/meta.js"; - -let cache: Meta; - -export function metaToPugArgs(meta: Meta): object { - let motd = ["Loading..."]; - if (meta.customMotd.length > 0) { - motd = meta.customMotd; - } - let splashIconUrl = meta.iconUrl; - if (meta.customSplashIcons.length > 0) { - splashIconUrl = - meta.customSplashIcons[ - Math.floor(Math.random() * meta.customSplashIcons.length) - ]; - } - - return { - img: meta.bannerUrl, - title: meta.name || "Firefish", - instanceName: meta.name || "Firefish", - desc: meta.description, - icon: meta.iconUrl, - splashIcon: splashIconUrl, - themeColor: meta.themeColor, - randomMOTD: motd[Math.floor(Math.random() * motd.length)], - privateMode: meta.privateMode, - }; -} - -export async function fetchMeta(noCache = false): Promise<Meta> { - if (!noCache && cache) return cache; - - return await db.transaction(async (transactionalEntityManager) => { - // New IDs are prioritized because multiple records may have been created due to past bugs. - const metas = await transactionalEntityManager.find(Meta, { - order: { - id: "DESC", - }, - }); - - const meta = metas[0]; - - if (meta) { - cache = meta; - return meta; - } else { - // If fetchMeta is called at the same time when meta is empty, this part may be called at the same time, so use fail-safe upsert. - const saved = await transactionalEntityManager - .upsert( - Meta, - { - id: "x", - }, - ["id"], - ) - .then((x) => - transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]), - ); - - cache = saved; - return saved; - } - }); -} - -setInterval(() => { - fetchMeta(true).then((meta) => { - cache = meta; - }); -}, 1000 * 10); diff --git a/packages/backend/src/misc/fetch-proxy-account.ts b/packages/backend/src/misc/fetch-proxy-account.ts index a277db6fb9..8d015da25d 100644 --- a/packages/backend/src/misc/fetch-proxy-account.ts +++ b/packages/backend/src/misc/fetch-proxy-account.ts @@ -1,9 +1,9 @@ -import { fetchMeta } from "./fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import type { ILocalUser } from "@/models/entities/user.js"; import { Users } from "@/models/index.js"; export async function fetchProxyAccount(): Promise<ILocalUser | null> { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.proxyAccountId == null) return null; return (await Users.findOneByOrFail({ id: meta.proxyAccountId, diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts index 4304d3b9e1..691d9743c8 100644 --- a/packages/backend/src/misc/reaction-lib.ts +++ b/packages/backend/src/misc/reaction-lib.ts @@ -1,5 +1,5 @@ import { emojiRegex } from "./emoji-regex.js"; -import { fetchMeta } from "./fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Emojis } from "@/models/index.js"; import { toPuny } from "backend-rs"; import { IsNull } from "typeorm"; @@ -21,7 +21,7 @@ export async function toDbReaction( reaction?: string | null, reacterHost?: string | null, ): Promise<string> { - if (!reaction) return (await fetchMeta()).defaultReaction; + if (!reaction) return (await fetchMeta(true)).defaultReaction; reacterHost = reacterHost == null ? null : toPuny(reacterHost); @@ -45,7 +45,7 @@ export async function toDbReaction( if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; } - return (await fetchMeta()).defaultReaction; + return (await fetchMeta(true)).defaultReaction; } type DecodedReaction = { diff --git a/packages/backend/src/misc/should-block-instance.ts b/packages/backend/src/misc/should-block-instance.ts index 35ed307931..465be41f2a 100644 --- a/packages/backend/src/misc/should-block-instance.ts +++ b/packages/backend/src/misc/should-block-instance.ts @@ -1,4 +1,4 @@ -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import type { Instance } from "@/models/entities/instance.js"; import type { Meta } from "@/models/entities/meta.js"; @@ -13,7 +13,7 @@ export async function shouldBlockInstance( host: Instance["host"], meta?: Meta, ): Promise<boolean> { - const { blockedHosts } = meta ?? (await fetchMeta()); + const { blockedHosts } = meta ?? (await fetchMeta(true)); return blockedHosts.some( (blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`), ); @@ -30,7 +30,7 @@ export async function shouldSilenceInstance( host: Instance["host"], meta?: Meta, ): Promise<boolean> { - const { silencedHosts } = meta ?? (await fetchMeta()); + const { silencedHosts } = meta ?? (await fetchMeta(true)); return silencedHosts.some( (silencedHost) => host === silencedHost || host.endsWith(`.${silencedHost}`), diff --git a/packages/backend/src/misc/skipped-instances.ts b/packages/backend/src/misc/skipped-instances.ts index 785393022a..14b26a3032 100644 --- a/packages/backend/src/misc/skipped-instances.ts +++ b/packages/backend/src/misc/skipped-instances.ts @@ -1,5 +1,5 @@ import { Brackets } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Instances } from "@/models/index.js"; import type { Instance } from "@/models/entities/instance.js"; import { DAY } from "@/const.js"; @@ -19,7 +19,7 @@ export async function skippedInstances( hosts: Instance["host"][], ): Promise<Instance["host"][]> { // first check for blocked instances since that info may already be in memory - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const shouldSkip = await Promise.all( hosts.map((host) => shouldBlockInstance(host, meta)), ); diff --git a/packages/backend/src/misc/translate.ts b/packages/backend/src/misc/translate.ts index e622171ec6..3395ce93be 100644 --- a/packages/backend/src/misc/translate.ts +++ b/packages/backend/src/misc/translate.ts @@ -1,7 +1,7 @@ import fetch from "node-fetch"; import { Converter } from "opencc-js"; import { getAgentByUrl } from "@/misc/fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import type { PostLanguage } from "@/misc/langmap"; import * as deepl from "deepl-node"; @@ -26,7 +26,7 @@ export async function translate( from: PostLanguage | null, to: PostLanguage, ) { - const instance = await fetchMeta(); + const instance = await fetchMeta(true); if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) { throw Error("No translator is set up on this server."); diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts index 46f9939a41..0ea72306b6 100644 --- a/packages/backend/src/queue/processors/inbox.ts +++ b/packages/backend/src/queue/processors/inbox.ts @@ -5,7 +5,7 @@ import perform from "@/remote/activitypub/perform.js"; import Logger from "@/services/logger.js"; import { registerOrFetchInstanceDoc } from "@/services/register-or-fetch-instance-doc.js"; import { Instances } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { toPuny, extractHost } from "backend-rs"; import { getApId } from "@/remote/activitypub/type.js"; import { fetchInstanceMetadata } from "@/services/fetch-instance-metadata.js"; @@ -41,7 +41,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => { const host = toPuny(new URL(signature.keyId).hostname); // interrupt if blocked - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (await shouldBlockInstance(host, meta)) { return `Blocked request: ${host}`; } diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index c0d40278ee..12ea63a931 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -1,7 +1,7 @@ import { URL } from "url"; import httpSignature, { IParsedSignature } from "@peertube/http-signature"; import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { toPuny } from "backend-rs"; import DbResolver from "@/remote/activitypub/db-resolver.js"; import { getApId } from "@/remote/activitypub/type.js"; @@ -12,7 +12,7 @@ import type { UserPublickey } from "@/models/entities/user-publickey.js"; import { verify } from "node:crypto"; export async function hasSignature(req: IncomingMessage): Promise<string> { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const required = meta.secureMode || meta.privateMode; try { @@ -27,7 +27,7 @@ export async function hasSignature(req: IncomingMessage): Promise<string> { } export async function checkFetch(req: IncomingMessage): Promise<number> { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { if (req.headers.host !== config.host) return 400; diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts index 2cf0c6c152..e2072a963a 100644 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ b/packages/backend/src/remote/activitypub/models/image.ts @@ -1,7 +1,7 @@ import { uploadFromUrl } from "@/services/drive/upload-from-url.js"; import type { CacheableRemoteUser } from "@/models/entities/user.js"; import Resolver from "../resolver.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { apLogger } from "../logger.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; import { DriveFiles } from "@/models/index.js"; @@ -34,7 +34,7 @@ export async function createImage( logger.info(`Creating the Image: ${image.url}`); - const instance = await fetchMeta(); + const instance = await fetchMeta(true); let file = await uploadFromUrl({ url: image.url, diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index 973f12cdc2..79b7962b72 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -1,7 +1,7 @@ import config from "@/config/index.js"; import type { ILocalUser } from "@/models/entities/user.js"; import { getInstanceActor } from "@/services/instance-actor.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { extractHost, isSelfHost } from "backend-rs"; import { apGet } from "./request.js"; import type { IObject, ICollection, IOrderedCollection } from "./type.js"; @@ -100,7 +100,7 @@ export default class Resolver { return await this.resolveLocal(value); } - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (await shouldBlockInstance(host, meta)) { throw new Error("Instance is blocked"); } diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 7e5fb5a281..00c8a6babe 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -9,7 +9,7 @@ import renderKey from "@/remote/activitypub/renderer/key.js"; import { renderPerson } from "@/remote/activitypub/renderer/person.js"; import renderEmoji from "@/remote/activitypub/renderer/emoji.js"; import { inbox as processInbox } from "@/queue/index.js"; -import { isSelfHost } from "backend-rs"; +import { fetchMeta, isSelfHost } from "backend-rs"; import { Notes, Users, @@ -25,7 +25,6 @@ import { getSignatureUser, } from "@/remote/activitypub/check-fetch.js"; import { getInstanceActor } from "@/services/instance-actor.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; import renderFollow from "@/remote/activitypub/renderer/follow.js"; import Featured from "./activitypub/featured.js"; import Following from "./activitypub/following.js"; @@ -238,7 +237,7 @@ router.get("/notes/:note", async (ctx, next) => { ctx.body = renderActivity(await renderNote(note, false)); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -268,7 +267,7 @@ router.get("/notes/:note/activity", async (ctx) => { } ctx.body = renderActivity(await packActivity(note)); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -323,7 +322,7 @@ router.get("/users/:user/publickey", async (ctx) => { if (Users.isLocalUser(user)) { ctx.body = renderActivity(renderKey(user, keypair)); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -343,7 +342,7 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) { } ctx.body = renderActivity(await renderPerson(user as ILocalUser)); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -426,8 +425,8 @@ router.get("/emojis/:emoji", async (ctx) => { return; } - ctx.body = renderActivity(await renderEmoji(emoji)); - const meta = await fetchMeta(); + ctx.body = renderActivity(renderEmoji(emoji)); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -459,7 +458,7 @@ router.get("/likes/:like", async (ctx) => { } ctx.body = renderActivity(await renderLike(reaction, note)); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -497,7 +496,7 @@ router.get( } ctx.body = renderActivity(renderFollow(follower, followee)); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -540,7 +539,7 @@ router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => { return; } - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { diff --git a/packages/backend/src/server/activitypub/featured.ts b/packages/backend/src/server/activitypub/featured.ts index 464a7f769d..e7ea6f238e 100644 --- a/packages/backend/src/server/activitypub/featured.ts +++ b/packages/backend/src/server/activitypub/featured.ts @@ -5,7 +5,7 @@ import renderOrderedCollection from "@/remote/activitypub/renderer/ordered-colle import renderNote from "@/remote/activitypub/renderer/note.js"; import { Users, Notes, UserNotePinings } from "@/models/index.js"; import { checkFetch } from "@/remote/activitypub/check-fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { setResponseType } from "../activitypub.js"; import type Router from "@koa/router"; @@ -57,7 +57,7 @@ export default async (ctx: Router.RouterContext) => { ctx.body = renderActivity(rendered); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts index 3c9e5fa201..576a672d6d 100644 --- a/packages/backend/src/server/activitypub/followers.ts +++ b/packages/backend/src/server/activitypub/followers.ts @@ -8,7 +8,7 @@ import renderFollowUser from "@/remote/activitypub/renderer/follow-user.js"; import { Users, Followings, UserProfiles } from "@/models/index.js"; import type { Following } from "@/models/entities/following.js"; import { checkFetch } from "@/remote/activitypub/check-fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { setResponseType } from "../activitypub.js"; import type { FindOptionsWhere } from "typeorm"; import type Router from "@koa/router"; @@ -110,7 +110,7 @@ export default async (ctx: Router.RouterContext) => { ctx.body = renderActivity(rendered); setResponseType(ctx); } - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts index cfbe985911..76b4e79716 100644 --- a/packages/backend/src/server/activitypub/following.ts +++ b/packages/backend/src/server/activitypub/following.ts @@ -8,7 +8,7 @@ import renderFollowUser from "@/remote/activitypub/renderer/follow-user.js"; import { Users, Followings, UserProfiles } from "@/models/index.js"; import type { Following } from "@/models/entities/following.js"; import { checkFetch } from "@/remote/activitypub/check-fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { setResponseType } from "../activitypub.js"; import type { FindOptionsWhere } from "typeorm"; import type Router from "@koa/router"; @@ -110,7 +110,7 @@ export default async (ctx: Router.RouterContext) => { ctx.body = renderActivity(rendered); setResponseType(ctx); } - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts index 53aa6f4ad5..305102cf12 100644 --- a/packages/backend/src/server/activitypub/outbox.ts +++ b/packages/backend/src/server/activitypub/outbox.ts @@ -11,7 +11,7 @@ import * as url from "@/prelude/url.js"; import { Users, Notes } from "@/models/index.js"; import type { Note } from "@/models/entities/note.js"; import { checkFetch } from "@/remote/activitypub/check-fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { makePaginationQuery } from "../api/common/make-pagination-query.js"; import { setResponseType } from "../activitypub.js"; import type Router from "@koa/router"; @@ -117,7 +117,7 @@ export default async (ctx: Router.RouterContext) => { setResponseType(ctx); } - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts index 620b754f30..5e65636427 100644 --- a/packages/backend/src/server/api/api-handler.ts +++ b/packages/backend/src/server/api/api-handler.ts @@ -2,7 +2,7 @@ import type Koa from "koa"; import type { User } from "@/models/entities/user.js"; import { UserIps } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import type { IEndpoint } from "./endpoints.js"; import authenticate, { AuthenticationError } from "./authenticate.js"; import call from "./call.js"; @@ -84,7 +84,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => // Log IP if (user) { - fetchMeta().then((meta) => { + fetchMeta(true).then((meta) => { if (!meta.enableIpLogging) return; const ip = ctx.ip; const ips = userIpHistories.get(user.id); diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index 2faef7b0e8..3107156a9b 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -10,7 +10,7 @@ import endpoints from "./endpoints.js"; import compatibility from "./compatibility.js"; import { ApiError } from "./error.js"; import { apiLogger } from "./logger.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; const accessDenied = { message: "Access denied.", @@ -117,7 +117,7 @@ export default async ( } // private mode - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if ( meta.privateMode && ep.meta.requireCredentialPrivateMode && diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index a22fbab8f1..c7731c6c81 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -1,5 +1,5 @@ import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js"; import define from "@/server/api/define.js"; @@ -466,7 +466,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); return { maintainerName: instance.maintainerName, diff --git a/packages/backend/src/server/api/endpoints/custom-motd.ts b/packages/backend/src/server/api/endpoints/custom-motd.ts index 2939355b94..ac1012258d 100644 --- a/packages/backend/src/server/api/endpoints/custom-motd.ts +++ b/packages/backend/src/server/api/endpoints/custom-motd.ts @@ -1,5 +1,5 @@ // import { IsNull } from 'typeorm'; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import define from "@/server/api/define.js"; export const meta = { @@ -27,7 +27,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const motd = await Promise.all(meta.customMotd.map((x) => x)); return motd; }); diff --git a/packages/backend/src/server/api/endpoints/custom-splash-icons.ts b/packages/backend/src/server/api/endpoints/custom-splash-icons.ts index f63a1b9600..4eb35aa3e5 100644 --- a/packages/backend/src/server/api/endpoints/custom-splash-icons.ts +++ b/packages/backend/src/server/api/endpoints/custom-splash-icons.ts @@ -1,5 +1,5 @@ // import { IsNull } from 'typeorm'; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import define from "@/server/api/define.js"; export const meta = { @@ -27,7 +27,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const icons = await Promise.all(meta.customSplashIcons.map((x) => x)); return icons; }); diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index 164e7b8f93..c04f219a9b 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -1,4 +1,4 @@ -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { DriveFiles } from "@/models/index.js"; import define from "@/server/api/define.js"; @@ -35,7 +35,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); // Calculate drive usage const usage = await DriveFiles.calcDriveUsageOf(user.id); diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index a3e3fafa2f..44e388a9bd 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -2,7 +2,7 @@ import { addFile } from "@/services/drive/add-file.js"; import { DriveFiles } from "@/models/index.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; import { IdentifiableError } from "@/misc/identifiable-error.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { MINUTE } from "@/const.js"; import define from "@/server/api/define.js"; import { apiLogger } from "@/server/api/logger.js"; @@ -96,7 +96,7 @@ export default define( name = null; } - const instanceMeta = await fetchMeta(); + const instanceMeta = await fetchMeta(true); try { // Create file diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 27a6dabb49..8c021d0e65 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -1,6 +1,6 @@ import define from "@/server/api/define.js"; import { Instances } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; export const meta = { @@ -101,7 +101,7 @@ export default define(meta, paramDef, async (ps, me) => { } if (typeof ps.blocked === "boolean") { - const meta = await fetchMeta(true); + const meta = await fetchMeta(false); if (ps.blocked) { if (meta.blockedHosts.length === 0) { return []; @@ -117,7 +117,7 @@ export default define(meta, paramDef, async (ps, me) => { } if (typeof ps.silenced === "boolean") { - const meta = await fetchMeta(true); + const meta = await fetchMeta(false); if (ps.silenced) { if (meta.silencedHosts.length === 0) { return []; diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index fe8bba95fd..9d31445a42 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -1,6 +1,6 @@ import { Brackets } from "typeorm"; import define from "@/server/api/define.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Notes } from "@/models/index.js"; import type { Note } from "@/models/entities/note.js"; import { safeForSql } from "@/misc/safe-for-sql.js"; @@ -67,7 +67,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); const hiddenTags = instance.hiddenTags.map((t) => normalizeForSearch(t)); const now = new Date(); // 5分単位で丸めた現在日時 diff --git a/packages/backend/src/server/api/endpoints/i/import-posts.ts b/packages/backend/src/server/api/endpoints/i/import-posts.ts index b8b52be98f..225306ebc5 100644 --- a/packages/backend/src/server/api/endpoints/i/import-posts.ts +++ b/packages/backend/src/server/api/endpoints/i/import-posts.ts @@ -3,7 +3,7 @@ import { createImportPostsJob } from "@/queue/index.js"; import { ApiError } from "@/server/api/error.js"; import { DriveFiles } from "@/models/index.js"; import { DAY } from "@/const.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; export const meta = { secure: true, @@ -45,7 +45,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const file = await DriveFiles.findOneBy({ id: ps.fileId }); - const instanceMeta = await fetchMeta(); + const instanceMeta = await fetchMeta(true); if (instanceMeta.experimentalFeatures?.postImports === false) throw new ApiError(meta.errors.importsDisabled); diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 2a674b52c3..31677ee2ef 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,7 +1,7 @@ import JSON5 from "json5"; import { IsNull, MoreThan } from "typeorm"; import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Ads, Emojis, Users } from "@/models/index.js"; import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js"; import define from "@/server/api/define.js"; @@ -398,7 +398,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); const emojis = await Emojis.find({ where: { diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 142b380f71..476375dc0b 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -1,4 +1,4 @@ -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Notes } from "@/models/index.js"; import { activeUsersChart } from "@/services/chart/index.js"; import define from "@/server/api/define.js"; @@ -64,7 +64,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); + const m = await fetchMeta(true); if (m.disableGlobalTimeline) { if (user == null || !(user.isAdmin || user.isModerator)) { throw new ApiError(meta.errors.gtlDisabled); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index c9800f2e1f..e6ab910040 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -1,5 +1,5 @@ import { Brackets } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Followings, Notes } from "@/models/index.js"; import { activeUsersChart } from "@/services/chart/index.js"; import define from "@/server/api/define.js"; @@ -71,7 +71,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); + const m = await fetchMeta(true); if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) { throw new ApiError(meta.errors.stlDisabled); } diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index b9cb68c2a0..2a99c1236c 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -1,5 +1,5 @@ import { Brackets } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Notes } from "@/models/index.js"; import { activeUsersChart } from "@/services/chart/index.js"; import define from "@/server/api/define.js"; @@ -74,7 +74,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); + const m = await fetchMeta(true); if (m.disableLocalTimeline) { if (user == null || !(user.isAdmin || user.isModerator)) { throw new ApiError(meta.errors.ltlDisabled); diff --git a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts b/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts index f71822f926..073a8f8569 100644 --- a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts @@ -1,5 +1,5 @@ import { Brackets } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Notes } from "@/models/index.js"; import { activeUsersChart } from "@/services/chart/index.js"; import define from "@/server/api/define.js"; @@ -74,7 +74,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); + const m = await fetchMeta(true); if (m.disableRecommendedTimeline) { if (user == null || !(user.isAdmin || user.isModerator)) { throw new ApiError(meta.errors.rtlDisabled); diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 6d6519e47b..325b54f350 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -1,6 +1,6 @@ import { IsNull } from "typeorm"; import { Users } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { stringToAcct } from "backend-rs"; import type { User } from "@/models/entities/user.js"; import define from "@/server/api/define.js"; @@ -31,7 +31,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const users = await Promise.all( meta.pinnedUsers diff --git a/packages/backend/src/server/api/endpoints/recommended-instances.ts b/packages/backend/src/server/api/endpoints/recommended-instances.ts index b235678428..5c5e267b2e 100644 --- a/packages/backend/src/server/api/endpoints/recommended-instances.ts +++ b/packages/backend/src/server/api/endpoints/recommended-instances.ts @@ -1,5 +1,5 @@ // import { IsNull } from 'typeorm'; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import define from "@/server/api/define.js"; export const meta = { @@ -27,7 +27,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const instances = await Promise.all(meta.recommendedInstances.map((x) => x)); return instances; }); diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index d3b6a08074..1a1ecad688 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -1,7 +1,7 @@ import * as os from "node:os"; import si from "systeminformation"; import define from "@/server/api/define.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; export const meta = { requireCredential: false, @@ -30,7 +30,7 @@ export default define(meta, paramDef, async () => { } } - const instanceMeta = await fetchMeta(); + const instanceMeta = await fetchMeta(true); if (!instanceMeta.enableServerMachineStats) { return { machine: "Not specified", diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index 528d3106e0..69b3f6779b 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -1,4 +1,4 @@ -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { genId } from "backend-rs"; import { SwSubscriptions } from "@/models/index.js"; import define from "@/server/api/define.js"; @@ -64,7 +64,7 @@ export default define(meta, paramDef, async (ps, me) => { publickey: ps.publickey, }); - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); // if already subscribed if (subscription != null) { diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 1b43762cb6..fda4aa0bb8 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -4,7 +4,7 @@ import { publishAdminStream } from "@/services/stream.js"; import { AbuseUserReports, UserProfiles, Users } from "@/models/index.js"; import { genId } from "backend-rs"; import { sendEmail } from "@/services/send-email.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { getUser } from "@/server/api/common/getters.js"; import { ApiError } from "@/server/api/error.js"; import define from "@/server/api/define.js"; @@ -86,7 +86,7 @@ export default define(meta, paramDef, async (ps, me) => { ], }); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); for (const moderator of moderators) { publishAdminStream(moderator.id, "newAbuseUserReport", { id: report.id, diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts index fbad7d5ef4..5c304929a1 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts @@ -1,6 +1,6 @@ import { Entity } from "megalodon"; import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Users, Notes } from "@/models/index.js"; import { IsNull } from "typeorm"; import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from "@/const.js"; @@ -10,7 +10,7 @@ export async function getInstance( contact: Entity.Account, ) { const [meta, totalUsers, totalStatuses] = await Promise.all([ - fetchMeta(), + fetchMeta(true), Users.count({ where: { host: IsNull() } }), Notes.count({ where: { userHost: IsNull() } }), ]); diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index 5e6c0edaae..5286c90fac 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -4,14 +4,13 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js"; import querystring from "node:querystring"; import qs from "qs"; import { convertTimelinesArgsId, limitToInt } from "./timeline.js"; -import { fromMastodonId } from "backend-rs"; +import { fetchMeta, fromMastodonId } from "backend-rs"; import { convertAccount, convertAttachment, convertPoll, convertStatus, } from "../converters.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; import { apiLogger } from "@/server/api/logger.js"; import { inspect } from "node:util"; @@ -213,7 +212,7 @@ export function apiStatusMastodon(router: Router): void { router.post<{ Params: { id: string } }>( "/v1/statuses/:id/favourite", async (ctx) => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); @@ -235,7 +234,7 @@ export function apiStatusMastodon(router: Router): void { router.post<{ Params: { id: string } }>( "/v1/statuses/:id/unfavourite", async (ctx) => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); diff --git a/packages/backend/src/server/api/private/signup.ts b/packages/backend/src/server/api/private/signup.ts index 64b5c57337..5af5d65b50 100644 --- a/packages/backend/src/server/api/private/signup.ts +++ b/packages/backend/src/server/api/private/signup.ts @@ -1,18 +1,17 @@ import type Koa from "koa"; import rndstr from "rndstr"; -import { fetchMeta } from "@/misc/fetch-meta.js"; import { verifyHcaptcha, verifyRecaptcha } from "@/misc/captcha.js"; import { Users, RegistrationTickets, UserPendings } from "@/models/index.js"; import { signup } from "@/server/api/common/signup.js"; import config from "@/config/index.js"; import { sendEmail } from "@/services/send-email.js"; -import { genId, hashPassword } from "backend-rs"; +import { fetchMeta, genId, hashPassword } from "backend-rs"; import { validateEmailForAccount } from "@/services/validate-email-for-account.js"; export default async (ctx: Koa.Context) => { const body = ctx.request.body; - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); // Verify *Captcha // ただしテスト時はこの機構は障害となるため無効にする diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 97295af57a..1760d5abf7 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -1,6 +1,5 @@ import Channel from "../channel.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { checkWordMute } from "backend-rs"; +import { checkWordMute, fetchMeta } from "backend-rs"; import { isInstanceMuted } from "@/misc/is-instance-muted.js"; import { isUserRelated } from "@/misc/is-user-related.js"; import type { Packed } from "@/misc/schema.js"; @@ -17,7 +16,7 @@ export default class extends Channel { } public async init(params: any) { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.disableGlobalTimeline) { if (this.user == null || !(this.user.isAdmin || this.user.isModerator)) return; diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 9052e7c2a5..5100a48efd 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -1,6 +1,5 @@ import Channel from "../channel.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { checkWordMute } from "backend-rs"; +import { checkWordMute, fetchMeta } from "backend-rs"; import { isUserRelated } from "@/misc/is-user-related.js"; import { isInstanceMuted } from "@/misc/is-instance-muted.js"; import type { Packed } from "@/misc/schema.js"; @@ -17,7 +16,7 @@ export default class extends Channel { } public async init(params: any) { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if ( meta.disableLocalTimeline && !this.user!.isAdmin && diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index bd31c94f9d..2c9a38d677 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -1,6 +1,5 @@ import Channel from "../channel.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { checkWordMute } from "backend-rs"; +import { checkWordMute, fetchMeta } from "backend-rs"; import { isUserRelated } from "@/misc/is-user-related.js"; import type { Packed } from "@/misc/schema.js"; @@ -16,7 +15,7 @@ export default class extends Channel { } public async init(params: any) { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.disableLocalTimeline) { if (this.user == null || !(this.user.isAdmin || this.user.isModerator)) return; diff --git a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts index 26c3cbfc68..5d0d6fc602 100644 --- a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts @@ -1,6 +1,5 @@ import Channel from "../channel.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { checkWordMute } from "backend-rs"; +import { checkWordMute, fetchMeta } from "backend-rs"; import { isUserRelated } from "@/misc/is-user-related.js"; import { isInstanceMuted } from "@/misc/is-instance-muted.js"; import type { Packed } from "@/misc/schema.js"; @@ -17,7 +16,7 @@ export default class extends Channel { } public async init(params: any) { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if ( meta.disableRecommendedTimeline && !this.user!.isAdmin && @@ -37,7 +36,7 @@ export default class extends Channel { // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または // チャンネルの投稿ではなく、全体公開のローカルの投稿 または // フォローしているチャンネルの投稿 の場合だけ - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if ( !( note.user.host != null && diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index 2a6dfdf674..6cf837b4ed 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -16,7 +16,7 @@ import { IsNull } from "typeorm"; import config from "@/config/index.js"; import Logger from "@/services/logger.js"; import { Users } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { genIdenticon } from "@/misc/gen-identicon.js"; import { createTemp } from "@/misc/create-temp.js"; import { stringToAcct } from "backend-rs"; @@ -126,7 +126,7 @@ router.get("/avatar/@:acct", async (ctx) => { }); router.get("/identicon/:x", async (ctx) => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.enableIdenticonGeneration) { const [temp, cleanup] = await createTemp(); await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts index 1cb8eb1eaf..7359878b19 100644 --- a/packages/backend/src/server/nodeinfo.ts +++ b/packages/backend/src/server/nodeinfo.ts @@ -1,6 +1,6 @@ import Router from "@koa/router"; import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Users, Notes } from "@/models/index.js"; import { IsNull, MoreThan } from "typeorm"; import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js"; @@ -27,7 +27,7 @@ const nodeinfo2 = async () => { const now = Date.now(); const [meta, total, activeHalfyear, activeMonth, localPosts] = await Promise.all([ - fetchMeta(true), + fetchMeta(false), Users.count({ where: { host: IsNull() } }), Users.count({ where: { diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index bb17cd279a..6473073370 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -10,13 +10,12 @@ import Router from "@koa/router"; import send from "koa-send"; import favicon from "koa-favicon"; import views from "@ladjs/koa-views"; -import sharp from "sharp"; import { createBullBoard } from "@bull-board/api"; import { BullAdapter } from "@bull-board/api/bullAdapter.js"; import { KoaAdapter } from "@bull-board/koa"; import { In, IsNull } from "typeorm"; -import { fetchMeta, metaToPugArgs } from "@/misc/fetch-meta.js"; +import { fetchMeta, metaToPugArgs } from "backend-rs"; import config from "@/config/index.js"; import { Users, @@ -326,7 +325,7 @@ const getFeed = async ( noRenotes: string, noReplies: string, ) => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.privateMode) { return; } @@ -475,7 +474,7 @@ const userPage: Router.Middleware = async (ctx, next) => { } const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const me = profile.fields ? profile.fields .filter((filed) => filed.value?.match(/^https?:/)) @@ -524,7 +523,7 @@ router.get("/notes/:note", async (ctx, next) => { const profile = await UserProfiles.findOneByOrFail({ userId: note.userId, }); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); await ctx.render("note", { ...metaToPugArgs(meta), note: _note, @@ -558,7 +557,7 @@ router.get("/posts/:note", async (ctx, next) => { if (note) { const _note = await Notes.pack(note); const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); await ctx.render("note", { ...metaToPugArgs(meta), note: _note, @@ -596,7 +595,7 @@ router.get("/@:user/pages/:page", async (ctx, next) => { if (page) { const _page = await Pages.pack(page); const profile = await UserProfiles.findOneByOrFail({ userId: page.userId }); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); await ctx.render("page", { ...metaToPugArgs(meta), page: _page, @@ -628,7 +627,7 @@ router.get("/clips/:clip", async (ctx, next) => { if (clip) { const _clip = await Clips.pack(clip); const profile = await UserProfiles.findOneByOrFail({ userId: clip.userId }); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); await ctx.render("clip", { ...metaToPugArgs(meta), clip: _clip, @@ -653,7 +652,7 @@ router.get("/gallery/:post", async (ctx, next) => { if (post) { const _post = await GalleryPosts.pack(post); const profile = await UserProfiles.findOneByOrFail({ userId: post.userId }); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); await ctx.render("gallery-post", { ...metaToPugArgs(meta), post: _post, @@ -679,7 +678,7 @@ router.get("/channels/:channel", async (ctx, next) => { if (channel) { const _channel = await Channels.pack(channel); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); await ctx.render("channel", { ...metaToPugArgs(meta), channel: _channel, @@ -732,7 +731,7 @@ router.get("/api/v1/streaming", async (ctx) => { // Render base html for all requests router.get("(.*)", async (ctx) => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); await ctx.render("base", { ...metaToPugArgs(meta), diff --git a/packages/backend/src/server/web/manifest.ts b/packages/backend/src/server/web/manifest.ts index bbcf639ffe..a4c615c7ab 100644 --- a/packages/backend/src/server/web/manifest.ts +++ b/packages/backend/src/server/web/manifest.ts @@ -1,5 +1,5 @@ import type Koa from "koa"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import config from "@/config/index.js"; import manifest from "./manifest.json" assert { type: "json" }; @@ -8,7 +8,7 @@ export const manifestHandler = async (ctx: Koa.Context) => { //const res = structuredClone(manifest); const res = JSON.parse(JSON.stringify(manifest)); - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); res.short_name = instance.name || "Firefish"; res.name = instance.name || "Firefish"; diff --git a/packages/backend/src/server/web/url-preview.ts b/packages/backend/src/server/web/url-preview.ts index 07d3bf7f2c..f59f3f357a 100644 --- a/packages/backend/src/server/web/url-preview.ts +++ b/packages/backend/src/server/web/url-preview.ts @@ -1,6 +1,6 @@ import type Koa from "koa"; import summaly from "summaly"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import Logger from "@/services/logger.js"; import config from "@/config/index.js"; import { query } from "@/prelude/url.js"; @@ -22,7 +22,7 @@ export const urlPreviewHandler = async (ctx: Koa.Context) => { return; } - const meta = await fetchMeta(); + const meta = await fetchMeta(true); logger.info( meta.summalyProxy diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 6320277eef..24ad9f8f02 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -6,7 +6,7 @@ import type S3 from "aws-sdk/clients/s3.js"; // TODO: migrate to SDK v3 import sharp from "sharp"; import { IsNull } from "typeorm"; import { publishMainStream, publishDriveStream } from "@/services/stream.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { contentDisposition } from "@/misc/content-disposition.js"; import { getFileInfo } from "@/misc/get-file-info.js"; import { @@ -77,7 +77,7 @@ async function save( // thunbnail, webpublic を必要なら生成 const alts = await generateAlts(path, type, !file.uri); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.useObjectStorage) { //#region ObjectStorage params @@ -360,7 +360,7 @@ async function upload( if (type === "image/apng") type = "image/png"; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = "application/octet-stream"; - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const params = { Bucket: meta.objectStorageBucket, @@ -495,7 +495,7 @@ export async function addFile({ const usage = await DriveFiles.calcDriveUsageOf(user); const u = await Users.findOneBy({ id: user.id }); - const instance = await fetchMeta(); + const instance = await fetchMeta(true); let driveCapacity = 1024 * 1024 * @@ -567,7 +567,7 @@ export async function addFile({ : null; const folder = await fetchFolder(); - const instance = await fetchMeta(); + const instance = await fetchMeta(true); let file = new DriveFile(); file.id = genId(); diff --git a/packages/backend/src/services/drive/delete-file.ts b/packages/backend/src/services/drive/delete-file.ts index 16c0219e71..b4b5580a1c 100644 --- a/packages/backend/src/services/drive/delete-file.ts +++ b/packages/backend/src/services/drive/delete-file.ts @@ -2,7 +2,7 @@ import type { DriveFile } from "@/models/entities/drive-file.js"; import { InternalStorage } from "./internal-storage.js"; import { DriveFiles } from "@/models/index.js"; import { createDeleteObjectStorageFileJob } from "@/queue/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { getS3 } from "./s3.js"; import { v4 as uuid } from "uuid"; @@ -82,7 +82,7 @@ async function postProcess(file: DriveFile, isExpired = false) { } export async function deleteObjectStorageFile(key: string) { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const s3 = getS3(meta); diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts index 09749059a9..1a772ff9c5 100644 --- a/packages/backend/src/services/push-notification.ts +++ b/packages/backend/src/services/push-notification.ts @@ -1,7 +1,7 @@ import push from "web-push"; import config from "@/config/index.js"; import { SwSubscriptions } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import type { Packed } from "@/misc/schema.js"; import { getNoteSummary } from "@/misc/get-note-summary.js"; @@ -45,7 +45,7 @@ export async function pushNotification<T extends keyof pushNotificationsTypes>( type: T, body: pushNotificationsTypes[T], ) { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if ( !meta.enableServiceWorker || diff --git a/packages/backend/src/services/send-email.ts b/packages/backend/src/services/send-email.ts index aa96cfc014..11a899d267 100644 --- a/packages/backend/src/services/send-email.ts +++ b/packages/backend/src/services/send-email.ts @@ -1,5 +1,5 @@ import * as nodemailer from "nodemailer"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import Logger from "@/services/logger.js"; import config from "@/config/index.js"; import { inspect } from "node:util"; @@ -12,7 +12,7 @@ export async function sendEmail( html: string, text: string, ) { - const meta = await fetchMeta(true); + const meta = await fetchMeta(false); const iconUrl = `${config.url}/static-assets/mi-white.png`; const emailSettingUrl = `${config.url}/settings/email`; diff --git a/packages/backend/src/services/validate-email-for-account.ts b/packages/backend/src/services/validate-email-for-account.ts index 4d05afcc6d..5aa091a5ac 100644 --- a/packages/backend/src/services/validate-email-for-account.ts +++ b/packages/backend/src/services/validate-email-for-account.ts @@ -1,12 +1,12 @@ import { validate as validateEmail } from "deep-email-validator"; import { UserProfiles } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; export async function validateEmailForAccount(emailAddress: string): Promise<{ available: boolean; reason: null | "used" | "format" | "disposable" | "mx" | "smtp"; }> { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const exist = await UserProfiles.countBy({ emailVerified: true, From fca48b2a816fcef6ad17ccb0c31a2c9a15c31e54 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 14 Apr 2024 20:29:44 +0900 Subject: [PATCH 058/110] refactor (backend): port safe-for-sql, sql-like-escape to backend-rs --- packages/backend-rs/index.d.ts | 2 ++ packages/backend-rs/index.js | 4 ++- packages/backend-rs/src/misc/escape_sql.rs | 36 +++++++++++++++++++ packages/backend-rs/src/misc/mod.rs | 1 + packages/backend/src/misc/safe-for-sql.ts | 3 -- packages/backend/src/misc/sql-like-escape.ts | 3 -- .../api/endpoints/admin/emoji/list-remote.ts | 3 +- .../server/api/endpoints/admin/emoji/list.ts | 4 +-- .../server/api/endpoints/admin/show-users.ts | 2 +- .../server/api/endpoints/channels/search.ts | 2 +- .../api/endpoints/federation/instances.ts | 3 +- .../server/api/endpoints/hashtags/search.ts | 2 +- .../server/api/endpoints/hashtags/trend.ts | 3 +- .../api/endpoints/notes/search-by-tag.ts | 2 +- .../src/server/api/endpoints/notes/search.ts | 4 +-- .../users/search-by-username-and-host.ts | 2 +- .../src/server/api/endpoints/users/search.ts | 2 +- 17 files changed, 55 insertions(+), 23 deletions(-) create mode 100644 packages/backend-rs/src/misc/escape_sql.rs delete mode 100644 packages/backend/src/misc/safe-for-sql.ts delete mode 100644 packages/backend/src/misc/sql-like-escape.ts diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 3770056d8f..a9398aacc1 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -132,6 +132,8 @@ export function isSelfHost(host?: string | undefined | null): boolean export function isSameOrigin(uri: string): boolean export function extractHost(uri: string): string export function toPuny(host: string): string +export function sqlLikeEscape(src: string): string +export function safeForSql(src: string): boolean /** Convert milliseconds to a human readable string */ export function formatMilliseconds(milliseconds: number): string export function toMastodonId(firefishId: string): string | null diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 363c858e4a..7a404d6447 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding module.exports.readServerConfig = readServerConfig module.exports.stringToAcct = stringToAcct @@ -321,6 +321,8 @@ module.exports.isSelfHost = isSelfHost module.exports.isSameOrigin = isSameOrigin module.exports.extractHost = extractHost module.exports.toPuny = toPuny +module.exports.sqlLikeEscape = sqlLikeEscape +module.exports.safeForSql = safeForSql module.exports.formatMilliseconds = formatMilliseconds module.exports.toMastodonId = toMastodonId module.exports.fromMastodonId = fromMastodonId diff --git a/packages/backend-rs/src/misc/escape_sql.rs b/packages/backend-rs/src/misc/escape_sql.rs new file mode 100644 index 0000000000..c575e088ce --- /dev/null +++ b/packages/backend-rs/src/misc/escape_sql.rs @@ -0,0 +1,36 @@ +#[crate::export] +pub fn sql_like_escape(src: &str) -> String { + src.replace('%', r"\%").replace('_', r"\_") +} + +#[crate::export] +pub fn safe_for_sql(src: &str) -> bool { + !src.contains([ + '\0', '\x08', '\x09', '\x1a', '\n', '\r', '"', '\'', '\\', '%', + ]) +} + +#[cfg(test)] +mod unit_test { + use super::{safe_for_sql, sql_like_escape}; + use pretty_assertions::assert_eq; + + #[test] + fn sql_like_escape_test() { + assert_eq!(sql_like_escape(""), ""); + assert_eq!(sql_like_escape("abc"), "abc"); + assert_eq!(sql_like_escape("a%bc"), r"a\%bc"); + assert_eq!(sql_like_escape("a呼%吸bc"), r"a呼\%吸bc"); + assert_eq!(sql_like_escape("a呼%吸b%_c"), r"a呼\%吸b\%\_c"); + assert_eq!(sql_like_escape("_اللغة العربية"), r"\_اللغة العربية"); + } + + #[test] + fn safe_for_sql_test() { + assert!(safe_for_sql("123")); + assert!(safe_for_sql("人間")); + assert!(!safe_for_sql("人間\x09")); + assert!(!safe_for_sql("abc\ndef")); + assert!(!safe_for_sql("%something%")); + } +} diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index 7f99a67324..74a483ea51 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -1,6 +1,7 @@ pub mod acct; pub mod check_word_mute; pub mod convert_host; +pub mod escape_sql; pub mod format_milliseconds; pub mod mastodon_id; pub mod meta; diff --git a/packages/backend/src/misc/safe-for-sql.ts b/packages/backend/src/misc/safe-for-sql.ts deleted file mode 100644 index 02eb7f0a26..0000000000 --- a/packages/backend/src/misc/safe-for-sql.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function safeForSql(text: string): boolean { - return !/[\0\x08\x09\x1a\n\r"'\\\%]/g.test(text); -} diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts deleted file mode 100644 index 453947d6ec..0000000000 --- a/packages/backend/src/misc/sql-like-escape.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function sqlLikeEscape(s: string) { - return s.replace(/([%_])/g, "\\$1"); -} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index 5c3e19d9e0..9c7a5180d3 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -1,8 +1,7 @@ import define from "@/server/api/define.js"; import { Emojis } from "@/models/index.js"; -import { toPuny } from "backend-rs"; +import { sqlLikeEscape, toPuny } from "backend-rs"; import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; import { ApiError } from "@/server/api/error.js"; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index 434b679608..98a69090db 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -1,8 +1,8 @@ import define from "@/server/api/define.js"; import { Emojis } from "@/models/index.js"; -import { makePaginationQuery } from "../../../common/make-pagination-query.js"; +import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; import type { Emoji } from "@/models/entities/emoji.js"; -//import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +//import { sqlLikeEscape } from "backend-rs"; import { ApiError } from "@/server/api/error.js"; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 1e6ebeda93..8a892c3606 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -1,6 +1,6 @@ import { Users } from "@/models/index.js"; import define from "@/server/api/define.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { sqlLikeEscape } from "backend-rs"; export const meta = { tags: ["admin"], diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts index b2fab701c5..ed44250a37 100644 --- a/packages/backend/src/server/api/endpoints/channels/search.ts +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -2,7 +2,7 @@ import define from "@/server/api/define.js"; import { Brackets } from "typeorm"; import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; import { Channels } from "@/models/index.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { sqlLikeEscape } from "backend-rs"; export const meta = { tags: ["channels"], diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 8c021d0e65..362ab098fb 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -1,7 +1,6 @@ import define from "@/server/api/define.js"; import { Instances } from "@/models/index.js"; -import { fetchMeta } from "backend-rs"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { fetchMeta, sqlLikeEscape } from "backend-rs"; export const meta = { tags: ["federation"], diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index 1dc1fb4922..8fb5b23f62 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -1,6 +1,6 @@ import define from "@/server/api/define.js"; import { Hashtags } from "@/models/index.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { sqlLikeEscape } from "backend-rs"; export const meta = { tags: ["hashtags"], diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index 9d31445a42..531a494248 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -1,9 +1,8 @@ import { Brackets } from "typeorm"; import define from "@/server/api/define.js"; -import { fetchMeta } from "backend-rs"; +import { fetchMeta, safeForSql } from "backend-rs"; import { Notes } from "@/models/index.js"; import type { Note } from "@/models/entities/note.js"; -import { safeForSql } from "@/misc/safe-for-sql.js"; import { normalizeForSearch } from "@/misc/normalize-for-search.js"; /* diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index e87725e342..f449ea081a 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -1,6 +1,6 @@ import { Brackets } from "typeorm"; import { Notes } from "@/models/index.js"; -import { safeForSql } from "@/misc/safe-for-sql.js"; +import { safeForSql } from "backend-rs"; import { normalizeForSearch } from "@/misc/normalize-for-search.js"; import define from "@/server/api/define.js"; import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index b159a91944..f28208cba9 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,11 +1,11 @@ import { Notes } from "@/models/index.js"; -import { Note } from "@/models/entities/note.js"; +import type { Note } from "@/models/entities/note.js"; import define from "@/server/api/define.js"; import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js"; import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js"; import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { sqlLikeEscape } from "backend-rs"; import type { SelectQueryBuilder } from "typeorm"; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 517ef615b1..fe15ae18c0 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -2,7 +2,7 @@ import { Brackets } from "typeorm"; import { Followings, Users } from "@/models/index.js"; import type { User } from "@/models/entities/user.js"; import define from "@/server/api/define.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { sqlLikeEscape } from "backend-rs"; export const meta = { tags: ["users"], diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index a15a0feb4b..df0701709b 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -2,7 +2,7 @@ import { Brackets } from "typeorm"; import { UserProfiles, Users } from "@/models/index.js"; import type { User } from "@/models/entities/user.js"; import define from "@/server/api/define.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { sqlLikeEscape } from "backend-rs"; export const meta = { tags: ["users"], From 21225f713716e72427c7cf05f0e55401e80cb2c9 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Mon, 15 Apr 2024 04:09:33 +0900 Subject: [PATCH 059/110] chore: update dependencies --- Cargo.lock | 81 +-- Cargo.toml | 2 +- package.json | 8 +- packages/backend/package.json | 12 +- packages/client/package.json | 6 +- pnpm-lock.yaml | 1132 +++++++++++++++++++-------------- 6 files changed, 727 insertions(+), 514 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6552351a3a..bc584faf24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,9 +59,9 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -436,9 +436,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" +checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" [[package]] name = "cfg-if" @@ -464,7 +464,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -695,9 +695,9 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" dependencies = [ "serde", ] @@ -1246,7 +1246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -1367,9 +1367,9 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9130fccc5f763cf2069b34a089a18f0d0883c66aceb81f2fad541a3d823c43" +checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" [[package]] name = "napi-derive" @@ -1421,9 +1421,9 @@ dependencies = [ [[package]] name = "num" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41" dependencies = [ "num-bigint", "num-complex", @@ -3123,7 +3123,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3141,7 +3141,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3161,17 +3161,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -3182,9 +3183,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -3194,9 +3195,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -3206,9 +3207,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -3218,9 +3225,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -3230,9 +3237,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -3242,9 +3249,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -3254,9 +3261,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" diff --git a/Cargo.toml b/Cargo.toml index 1a72cb2c5a..c9efe1f69a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ macro_rs = { path = "packages/macro-rs" } napi = { version = "2.16.2", default-features = false } napi-derive = "2.16.2" -napi-build = "2.1.2" +napi-build = "2.1.3" argon2 = "0.5.3" async-trait = "0.1.80" diff --git a/package.json b/package.json index a7bb0a5cc1..dd0766bc7b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "https://firefish.dev/firefish/firefish.git" }, - "packageManager": "pnpm@8.15.6", + "packageManager": "pnpm@8.15.7", "private": true, "scripts": { "rebuild": "pnpm run clean && pnpm run build", @@ -36,11 +36,11 @@ "clean-all": "pnpm run clean && pnpm run clean-cargo && pnpm run clean-npm" }, "dependencies": { - "js-yaml": "4.1.0", "gulp": "4.0.2", "gulp-cssnano": "2.1.3", "gulp-replace": "1.1.4", - "gulp-terser": "2.1.0" + "gulp-terser": "2.1.0", + "js-yaml": "4.1.0" }, "devDependencies": { "@biomejs/biome": "1.6.4", @@ -50,7 +50,7 @@ "@biomejs/cli-linux-x64": "^1.6.4", "@types/node": "20.12.7", "execa": "8.0.1", - "pnpm": "8.15.6", + "pnpm": "8.15.7", "typescript": "5.4.5" } } diff --git a/packages/backend/package.json b/packages/backend/package.json index 5dbf7cad64..bf435ec64b 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -22,9 +22,9 @@ "@swc/core-android-arm64": "1.3.11" }, "dependencies": { - "@bull-board/api": "5.15.3", - "@bull-board/koa": "5.15.3", - "@bull-board/ui": "5.15.3", + "@bull-board/api": "5.15.5", + "@bull-board/koa": "5.15.5", + "@bull-board/ui": "5.15.5", "@discordapp/twemoji": "^15.0.3", "@koa/cors": "5.0.0", "@koa/multer": "3.0.2", @@ -37,7 +37,7 @@ "adm-zip": "0.5.10", "ajv": "8.12.0", "archiver": "7.0.1", - "aws-sdk": "2.1597.0", + "aws-sdk": "2.1599.0", "axios": "^1.6.8", "backend-rs": "workspace:*", "blurhash": "2.0.5", @@ -52,7 +52,7 @@ "date-fns": "3.6.0", "decompress": "^4.2.1", "deep-email-validator": "0.1.21", - "deepl-node": "1.12.0", + "deepl-node": "1.13.0", "escape-regexp": "0.0.1", "feed": "4.2.2", "file-type": "19.0.0", @@ -98,7 +98,7 @@ "punycode": "2.3.1", "pureimage": "0.4.13", "qrcode": "1.5.3", - "qs": "6.12.0", + "qs": "6.12.1", "random-seed": "0.3.0", "ratelimiter": "3.4.1", "redis-semaphore": "5.5.1", diff --git a/packages/client/package.json b/packages/client/package.json index 594f07c607..97a5f83ef7 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -67,9 +67,9 @@ "photoswipe": "5.4.3", "prismjs": "1.29.0", "punycode": "2.3.1", - "rollup": "4.14.1", + "rollup": "4.14.2", "s-age": "1.1.2", - "sass": "1.74.1", + "sass": "1.75.0", "seedrandom": "3.0.5", "stringz": "2.1.0", "swiper": "11.1.1", @@ -88,6 +88,6 @@ "vue-draggable-plus": "^0.4.0", "vue-plyr": "^7.0.0", "vue-prism-editor": "2.0.0-alpha.2", - "vue-tsc": "2.0.12" + "vue-tsc": "2.0.13" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02b473ddce..53b6089f9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,8 +46,8 @@ importers: specifier: 8.0.1 version: 8.0.1 pnpm: - specifier: 8.15.6 - version: 8.15.6 + specifier: 8.15.7 + version: 8.15.7 typescript: specifier: 5.4.5 version: 5.4.5 @@ -55,14 +55,14 @@ importers: packages/backend: dependencies: '@bull-board/api': - specifier: 5.15.3 - version: 5.15.3(@bull-board/ui@5.15.3) + specifier: 5.15.5 + version: 5.15.5(@bull-board/ui@5.15.5) '@bull-board/koa': - specifier: 5.15.3 - version: 5.15.3(@types/koa@2.15.0)(pug@3.0.2) + specifier: 5.15.5 + version: 5.15.5(@types/koa@2.15.0)(pug@3.0.2) '@bull-board/ui': - specifier: 5.15.3 - version: 5.15.3 + specifier: 5.15.5 + version: 5.15.5 '@discordapp/twemoji': specifier: ^15.0.3 version: 15.0.3 @@ -100,8 +100,8 @@ importers: specifier: 7.0.1 version: 7.0.1 aws-sdk: - specifier: 2.1597.0 - version: 2.1597.0 + specifier: 2.1599.0 + version: 2.1599.0 axios: specifier: ^1.6.8 version: 1.6.8 @@ -145,8 +145,8 @@ importers: specifier: 0.1.21 version: 0.1.21 deepl-node: - specifier: 1.12.0 - version: 1.12.0 + specifier: 1.13.0 + version: 1.13.0 escape-regexp: specifier: 0.0.1 version: 0.0.1 @@ -283,8 +283,8 @@ importers: specifier: 1.5.3 version: 1.5.3 qs: - specifier: 6.12.0 - version: 6.12.0 + specifier: 6.12.1 + version: 6.12.1 random-seed: specifier: 0.3.0 version: 0.3.0 @@ -547,22 +547,22 @@ importers: devDependencies: '@eslint-sets/eslint-config-vue3': specifier: ^5.12.0 - version: 5.12.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5) + version: 5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5) '@eslint-sets/eslint-config-vue3-ts': specifier: ^3.3.0 - version: 3.3.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5) + version: 3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5) '@phosphor-icons/web': specifier: ^2.1.1 version: 2.1.1 '@rollup/plugin-alias': specifier: 5.1.0 - version: 5.1.0(rollup@4.14.1) + version: 5.1.0(rollup@4.14.2) '@rollup/plugin-json': specifier: 6.1.0 - version: 6.1.0(rollup@4.14.1) + version: 6.1.0(rollup@4.14.2) '@rollup/pluginutils': specifier: ^5.1.0 - version: 5.1.0(rollup@4.14.1) + version: 5.1.0(rollup@4.14.2) '@syuilo/aiscript': specifier: 0.17.0 version: 0.17.0 @@ -658,7 +658,7 @@ importers: version: 3.0.12 eslint-plugin-file-progress: specifier: ^1.3.0 - version: 1.3.0(eslint@8.57.0) + version: 1.3.0(eslint@9.0.0) eventemitter3: specifier: 5.0.1 version: 5.0.1 @@ -711,14 +711,14 @@ importers: specifier: 2.3.1 version: 2.3.1 rollup: - specifier: 4.14.1 - version: 4.14.1 + specifier: 4.14.2 + version: 4.14.2 s-age: specifier: 1.1.2 version: 1.1.2 sass: - specifier: 1.74.1 - version: 1.74.1 + specifier: 1.75.0 + version: 1.75.0 seedrandom: specifier: 3.0.5 version: 3.0.5 @@ -757,7 +757,7 @@ importers: version: 9.0.1 vite: specifier: 5.2.8 - version: 5.2.8(@types/node@20.12.7)(sass@1.74.1) + version: 5.2.8(@types/node@20.12.7)(sass@1.75.0) vite-plugin-compression: specifier: ^0.5.1 version: 0.5.1(vite@5.2.8) @@ -766,7 +766,7 @@ importers: version: 3.4.21(typescript@5.4.5) vue-draggable-plus: specifier: ^0.4.0 - version: 0.4.0(@types/sortablejs@1.15.4) + version: 0.4.0(@types/sortablejs@1.15.8) vue-plyr: specifier: ^7.0.0 version: 7.0.0 @@ -774,8 +774,8 @@ importers: specifier: 2.0.0-alpha.2 version: 2.0.0-alpha.2(vue@3.4.21) vue-tsc: - specifier: 2.0.12 - version: 2.0.12(typescript@5.4.5) + specifier: 2.0.13 + version: 2.0.13(typescript@5.4.5) packages/firefish-js: dependencies: @@ -931,7 +931,7 @@ importers: version: 4.17.21 ts-jest: specifier: ^29.0.5 - version: 29.1.1(@babel/core@7.23.2)(jest@29.7.0)(typescript@4.9.4) + version: 29.1.1(@babel/core@7.24.4)(jest@29.7.0)(typescript@4.9.4) typedoc: specifier: ^0.23.24 version: 0.23.28(typescript@4.9.4) @@ -949,7 +949,7 @@ importers: version: 6.2.1 vite: specifier: 5.2.8 - version: 5.2.8(@types/node@20.12.7)(sass@1.74.1) + version: 5.2.8(@types/node@20.12.7)(sass@1.75.0) vite-plugin-compression: specifier: ^0.5.1 version: 0.5.1(vite@5.2.8) @@ -968,6 +968,14 @@ packages: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.20 + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + /@babel/code-frame@7.22.10: resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==} engines: {node: '>=6.9.0'} @@ -983,6 +991,14 @@ packages: chalk: 2.4.2 dev: true + /@babel/code-frame@7.24.2: + resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.24.2 + picocolors: 1.0.0 + dev: true + /@babel/compat-data@7.22.9: resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} engines: {node: '>=6.9.0'} @@ -992,6 +1008,11 @@ packages: engines: {node: '>=6.9.0'} dev: true + /@babel/compat-data@7.24.4: + resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/core@7.22.10: resolution: {integrity: sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw==} engines: {node: '>=6.9.0'} @@ -1037,30 +1058,53 @@ packages: - supports-color dev: true - /@babel/eslint-parser@7.23.10(@babel/core@7.23.2)(eslint@8.57.0): + /@babel/core@7.24.4: + resolution: {integrity: sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.2 + '@babel/generator': 7.24.4 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4) + '@babel/helpers': 7.24.4 + '@babel/parser': 7.24.4 + '@babel/template': 7.24.0 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 + convert-source-map: 2.0.0 + debug: 4.3.4(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/eslint-parser@7.23.10(@babel/core@7.24.4)(eslint@9.0.0): resolution: {integrity: sha512-3wSYDPZVnhseRnxRJH6ZVTNknBz76AEnyC+AYYhasjP3Yy23qz0ERR7Fcd2SHmYuSFJ2kY9gaaDd3vyqU09eSw==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: '@babel/core': ^7.11.0 eslint: ^7.5.0 || ^8.0.0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 8.57.0 + eslint: 9.0.0 eslint-visitor-keys: 2.1.0 semver: 6.3.1 dev: true - /@babel/eslint-parser@7.23.3(@babel/core@7.23.2)(eslint@8.57.0): + /@babel/eslint-parser@7.23.3(@babel/core@7.24.4)(eslint@9.0.0): resolution: {integrity: sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: '@babel/core': ^7.11.0 eslint: ^7.5.0 || ^8.0.0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 8.57.0 + eslint: 9.0.0 eslint-visitor-keys: 2.1.0 semver: 6.3.1 dev: true @@ -1084,6 +1128,16 @@ packages: jsesc: 2.5.2 dev: true + /@babel/generator@7.24.4: + resolution: {integrity: sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + dev: true + /@babel/helper-compilation-targets@7.22.10: resolution: {integrity: sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==} engines: {node: '>=6.9.0'} @@ -1105,6 +1159,17 @@ packages: semver: 6.3.1 dev: true + /@babel/helper-compilation-targets@7.23.6: + resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.24.4 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.23.0 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + /@babel/helper-environment-visitor@7.22.20: resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} engines: {node: '>=6.9.0'} @@ -1148,6 +1213,13 @@ packages: dependencies: '@babel/types': 7.23.0 + /@babel/helper-module-imports@7.24.3: + resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + dev: true + /@babel/helper-module-transforms@7.22.9(@babel/core@7.22.10): resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==} engines: {node: '>=6.9.0'} @@ -1175,6 +1247,20 @@ packages: '@babel/helper-validator-identifier': 7.22.20 dev: true + /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.4): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.24.3 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + /@babel/helper-plugin-utils@7.22.5: resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} engines: {node: '>=6.9.0'} @@ -1195,6 +1281,11 @@ packages: resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} engines: {node: '>=6.9.0'} + /@babel/helper-string-parser@7.24.1: + resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} @@ -1212,6 +1303,11 @@ packages: resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==} engines: {node: '>=6.9.0'} + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helpers@7.22.10: resolution: {integrity: sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==} engines: {node: '>=6.9.0'} @@ -1233,6 +1329,17 @@ packages: - supports-color dev: true + /@babel/helpers@7.24.4: + resolution: {integrity: sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.0 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/highlight@7.22.10: resolution: {integrity: sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==} engines: {node: '>=6.9.0'} @@ -1250,6 +1357,16 @@ packages: js-tokens: 4.0.0 dev: true + /@babel/highlight@7.24.2: + resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.0 + dev: true + /@babel/parser@7.22.10: resolution: {integrity: sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==} engines: {node: '>=6.0.0'} @@ -1272,6 +1389,14 @@ packages: '@babel/types': 7.23.0 dev: true + /@babel/parser@7.24.4: + resolution: {integrity: sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.0 + dev: true + /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.22.10): resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} engines: {node: '>=6.9.0'} @@ -1457,6 +1582,15 @@ packages: '@babel/parser': 7.23.0 '@babel/types': 7.23.0 + /@babel/template@7.24.0: + resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.2 + '@babel/parser': 7.24.4 + '@babel/types': 7.24.0 + dev: true + /@babel/traverse@7.22.10: resolution: {integrity: sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==} engines: {node: '>=6.9.0'} @@ -1492,6 +1626,24 @@ packages: - supports-color dev: true + /@babel/traverse@7.24.1: + resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.2 + '@babel/generator': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.24.4 + '@babel/types': 7.24.0 + debug: 4.3.4(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/types@7.22.10: resolution: {integrity: sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==} engines: {node: '>=6.9.0'} @@ -1508,6 +1660,15 @@ packages: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 + /@babel/types@7.24.0: + resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.1 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true @@ -1592,26 +1753,26 @@ packages: dev: true optional: true - /@bull-board/api@5.15.3(@bull-board/ui@5.15.3): - resolution: {integrity: sha512-tlEYOI6Hp0ZGozDCtKQEFgvzTKXj+drKStHJm86s1TcUZlsnMjzR0BUxu5CW6EB3tS3MtPLJH5RQCmUq0UEiiQ==} + /@bull-board/api@5.15.5(@bull-board/ui@5.15.5): + resolution: {integrity: sha512-s3x0f+0s4nwndBM+QSROMVKiDyE/vaaouQCsxRWOFqneLCkM+Ro2wF6fkhmFkZMjouoBbS8rCFGaIZ+8uttYtg==} peerDependencies: - '@bull-board/ui': 5.15.3 + '@bull-board/ui': 5.15.5 dependencies: - '@bull-board/ui': 5.15.3 + '@bull-board/ui': 5.15.5 redis-info: 3.1.0 dev: false - /@bull-board/koa@5.15.3(@types/koa@2.15.0)(pug@3.0.2): - resolution: {integrity: sha512-pgHdRcre8RJKwWqlMLFY1oj742hLtxVrHsT2s4k+Ribzmoj3bq1tgRHtu6m9TX7AyABBtcTfTo30NNbPPrYR7A==} + /@bull-board/koa@5.15.5(@types/koa@2.15.0)(pug@3.0.2): + resolution: {integrity: sha512-Kbmca8hKNW5wLpGM/H1RBm09bcdK+KWCsINUyDtp91bGwMRK0mhiqvjJLJpRohXXmtPTnnJDuVO9p1gYsbed3Q==} dependencies: - '@bull-board/api': 5.15.3(@bull-board/ui@5.15.3) - '@bull-board/ui': 5.15.3 - ejs: 3.1.9 + '@bull-board/api': 5.15.5(@bull-board/ui@5.15.5) + '@bull-board/ui': 5.15.5 + ejs: 3.1.10 koa: 2.15.3 koa-mount: 4.0.0 koa-router: 10.1.1 koa-static: 5.0.0 - koa-views: 7.0.2(@types/koa@2.15.0)(ejs@3.1.9)(pug@3.0.2) + koa-views: 7.0.2(@types/koa@2.15.0)(ejs@3.1.10)(pug@3.0.2) transitivePeerDependencies: - '@types/koa' - arc-templates @@ -1669,10 +1830,10 @@ packages: - whiskers dev: false - /@bull-board/ui@5.15.3: - resolution: {integrity: sha512-wCXk+s4cSszZe0p0sYYxZPLSKafFQNPsUypTvpAh3IC2p4fr6F/wUBGb1kBMspRkFC19l5yFCD5qPHVlAR0QKw==} + /@bull-board/ui@5.15.5: + resolution: {integrity: sha512-TSXgqBDI3ig6ez6yHArGzpwCuA/rhQewv0KOUAvPzssgX4HqfkatrV7gTuTM+XJe7/sLiXnBiryV7SRV0hgRMg==} dependencies: - '@bull-board/api': 5.15.3(@bull-board/ui@5.15.3) + '@bull-board/api': 5.15.5(@bull-board/ui@5.15.5) dev: false /@cbor-extract/cbor-extract-darwin-arm64@2.2.0: @@ -1841,8 +2002,8 @@ packages: universalify: 0.1.2 dev: false - /@emnapi/runtime@1.1.0: - resolution: {integrity: sha512-gCGlE0fJGWalfy+wbFApjhKn6uoSVvopru77IPyxNKkjkaiSx2HxDS7eOYSmo9dcMIhmmIvoxiC3N9TM1c3EaA==} + /@emnapi/runtime@1.1.1: + resolution: {integrity: sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==} requiresBuild: true dependencies: tslib: 2.6.2 @@ -2075,16 +2236,6 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - dependencies: - eslint: 8.57.0 - eslint-visitor-keys: 3.4.3 - dev: true - /@eslint-community/eslint-utils@4.4.0(eslint@9.0.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2105,29 +2256,29 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true - /@eslint-sets/eslint-config-basic@3.3.0(@babel/core@7.23.2)(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(prettier@3.2.5): + /@eslint-sets/eslint-config-basic@3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(prettier@3.2.5): resolution: {integrity: sha512-x5YH0CvZJxn19/5ehu188XaoLQpxOGlFiIuPHCN6FyONgrmriakT/cmIIBOJg2Vi/y1bn2xbhsgVNb00J3HyTg==} peerDependencies: eslint: '>=8.0.0' prettier: '>=2.0.0' dependencies: - '@babel/eslint-parser': 7.23.3(@babel/core@7.23.2)(eslint@8.57.0) - eslint: 8.57.0 - eslint-config-prettier: 8.9.0(eslint@8.57.0) - eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) + '@babel/eslint-parser': 7.23.3(@babel/core@7.24.4)(eslint@9.0.0) + eslint: 9.0.0 + eslint-config-prettier: 8.9.0(eslint@9.0.0) + eslint-plugin-eslint-comments: 3.2.0(eslint@9.0.0) eslint-plugin-html: 7.1.0 - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0) - eslint-plugin-jsonc: 2.10.0(eslint@8.57.0) - eslint-plugin-markdown: 3.0.1(eslint@8.57.0) - eslint-plugin-n: 15.7.0(eslint@8.57.0) - eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.57.0)(prettier@3.2.5) - eslint-plugin-promise: 5.2.0(eslint@8.57.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0) + eslint-plugin-jsonc: 2.10.0(eslint@9.0.0) + eslint-plugin-markdown: 3.0.1(eslint@9.0.0) + eslint-plugin-n: 15.7.0(eslint@9.0.0) + eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5) + eslint-plugin-promise: 5.2.0(eslint@9.0.0) eslint-plugin-tsdoc: 0.2.17 - eslint-plugin-unicorn: 45.0.2(eslint@8.57.0) - eslint-plugin-yml: 1.10.0(eslint@8.57.0) + eslint-plugin-unicorn: 45.0.2(eslint@9.0.0) + eslint-plugin-yml: 1.10.0(eslint@9.0.0) jsonc-eslint-parser: 2.3.0 prettier: 3.2.5 - vue-eslint-parser: 9.3.2(eslint@8.57.0) + vue-eslint-parser: 9.3.2(eslint@9.0.0) yaml-eslint-parser: 1.2.2 transitivePeerDependencies: - '@babel/core' @@ -2137,7 +2288,7 @@ packages: - supports-color dev: true - /@eslint-sets/eslint-config-basic@5.12.0(@babel/core@7.23.2)(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5): + /@eslint-sets/eslint-config-basic@5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5): resolution: {integrity: sha512-AgECfmJsiVOWKmvgjv780VuuoT9SE6PRgxGTtytHSfE9b9MAJjHxToVTKtD4UEKvocEGbg2EcwqGbff8cxDWKw==} peerDependencies: eslint: '>=7.4.0' @@ -2147,24 +2298,24 @@ packages: typescript: optional: true dependencies: - '@babel/eslint-parser': 7.23.10(@babel/core@7.23.2)(eslint@8.57.0) - eslint: 8.57.0 - eslint-config-prettier: 9.1.0(eslint@8.57.0) - eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) + '@babel/eslint-parser': 7.23.10(@babel/core@7.24.4)(eslint@9.0.0) + eslint: 9.0.0 + eslint-config-prettier: 9.1.0(eslint@9.0.0) + eslint-plugin-eslint-comments: 3.2.0(eslint@9.0.0) eslint-plugin-html: 7.1.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.57.0) - eslint-plugin-jsonc: 2.13.0(eslint@8.57.0) - eslint-plugin-markdown: 3.0.1(eslint@8.57.0) - eslint-plugin-n: 16.6.2(eslint@8.57.0) - eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) - eslint-plugin-promise: 6.1.1(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@9.0.0) + eslint-plugin-jsonc: 2.13.0(eslint@9.0.0) + eslint-plugin-markdown: 3.0.1(eslint@9.0.0) + eslint-plugin-n: 16.6.2(eslint@9.0.0) + eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5) + eslint-plugin-promise: 6.1.1(eslint@9.0.0) eslint-plugin-tsdoc: 0.2.17 - eslint-plugin-unicorn: 40.1.0(eslint@8.57.0) - eslint-plugin-yml: 1.12.2(eslint@8.57.0) + eslint-plugin-unicorn: 40.1.0(eslint@9.0.0) + eslint-plugin-yml: 1.12.2(eslint@9.0.0) jsonc-eslint-parser: 2.4.0 prettier: 3.2.5 typescript: 5.4.5 - vue-eslint-parser: 9.4.2(eslint@8.57.0) + vue-eslint-parser: 9.4.2(eslint@9.0.0) yaml-eslint-parser: 1.2.2 transitivePeerDependencies: - '@babel/core' @@ -2175,19 +2326,19 @@ packages: - supports-color dev: true - /@eslint-sets/eslint-config-ts@3.3.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5): + /@eslint-sets/eslint-config-ts@3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5): resolution: {integrity: sha512-4Vj3KxYx16hmW6AyEv1mil0gVN8H3rdJt8TRWufbAj0ZN+EjwOPf3TqE7ASCYto/NpA8xWQY3NGm/og9Or/dDQ==} peerDependencies: eslint: '>=8.0.0' prettier: '>=2.0.0' typescript: '>=4.0.0' dependencies: - '@eslint-sets/eslint-config-basic': 3.3.0(@babel/core@7.23.2)(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(prettier@3.2.5) - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.57.0 - eslint-config-prettier: 8.9.0(eslint@8.57.0) - eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.57.0)(prettier@3.2.5) + '@eslint-sets/eslint-config-basic': 3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(prettier@3.2.5) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5) + eslint: 9.0.0 + eslint-config-prettier: 8.9.0(eslint@9.0.0) + eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5) eslint-plugin-tsdoc: 0.2.17 prettier: 3.2.5 typescript: 5.4.5 @@ -2198,7 +2349,7 @@ packages: - supports-color dev: true - /@eslint-sets/eslint-config-ts@5.12.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5): + /@eslint-sets/eslint-config-ts@5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5): resolution: {integrity: sha512-7vOzV6qYv0SbA9W17m9lkG/Zv+qVeCcAbWEY1d9hUbBHx9Ip48kNMNVDrnh97zUORXGcmjxsZ81W2lC36Ox2pw==} peerDependencies: eslint: '>=7.4.0' @@ -2208,12 +2359,12 @@ packages: typescript: optional: true dependencies: - '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.23.2)(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5) - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.57.0 - eslint-config-prettier: 9.1.0(eslint@8.57.0) - eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) + '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5) + eslint: 9.0.0 + eslint-config-prettier: 9.1.0(eslint@9.0.0) + eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5) eslint-plugin-tsdoc: 0.2.17 prettier: 3.2.5 typescript: 5.4.5 @@ -2225,26 +2376,26 @@ packages: - supports-color dev: true - /@eslint-sets/eslint-config-vue3-ts@3.3.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5): + /@eslint-sets/eslint-config-vue3-ts@3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5): resolution: {integrity: sha512-KX3VFuS5U4FYKfZ6PABQjl54BMpNapNjYYe103Nm2Zy8y9zphDCBAARbhU97XNSvzkurve7HhJcsi9gXrWlGFA==} peerDependencies: eslint: '>=8.0.0' prettier: '>=2.0.0' typescript: '>=4.0.0' dependencies: - '@eslint-sets/eslint-config-ts': 3.3.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5) - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.57.0 - eslint-config-prettier: 8.9.0(eslint@8.57.0) - eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.57.0)(prettier@3.2.5) + '@eslint-sets/eslint-config-ts': 3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5) + eslint: 9.0.0 + eslint-config-prettier: 8.9.0(eslint@9.0.0) + eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5) eslint-plugin-tsdoc: 0.2.17 eslint-plugin-vitest-globals: 1.4.0 - eslint-plugin-vue: 9.16.1(eslint@8.57.0) - eslint-plugin-vue-scoped-css: 2.5.0(eslint@8.57.0)(vue-eslint-parser@9.3.1) + eslint-plugin-vue: 9.16.1(eslint@9.0.0) + eslint-plugin-vue-scoped-css: 2.5.0(eslint@9.0.0)(vue-eslint-parser@9.3.1) prettier: 3.2.5 typescript: 5.4.5 - vue-eslint-parser: 9.3.1(eslint@8.57.0) + vue-eslint-parser: 9.3.1(eslint@9.0.0) transitivePeerDependencies: - '@babel/core' - eslint-import-resolver-typescript @@ -2252,7 +2403,7 @@ packages: - supports-color dev: true - /@eslint-sets/eslint-config-vue3@5.12.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5): + /@eslint-sets/eslint-config-vue3@5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5): resolution: {integrity: sha512-gQBmQicZihPcxncIdkKagQGZ2dH+97ioAlUpsaczEdgY9pLrLOU5oGTetjbaxAp6zGS2sXm1n0i2BnwRIlt4Bg==} peerDependencies: eslint: '>=7.4.0' @@ -2262,22 +2413,22 @@ packages: typescript: optional: true dependencies: - '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.23.2)(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5) - '@eslint-sets/eslint-config-ts': 5.12.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5) - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.57.0 - eslint-config-prettier: 9.1.0(eslint@8.57.0) - eslint-plugin-jsdoc: 48.0.6(eslint@8.57.0) - eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) + '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5) + '@eslint-sets/eslint-config-ts': 5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5) + eslint: 9.0.0 + eslint-config-prettier: 9.1.0(eslint@9.0.0) + eslint-plugin-jsdoc: 48.0.6(eslint@9.0.0) + eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5) eslint-plugin-tsdoc: 0.2.17 eslint-plugin-vitest-globals: 1.4.0 - eslint-plugin-vue: 9.21.1(eslint@8.57.0) - eslint-plugin-vue-scoped-css: 2.7.2(eslint@8.57.0)(vue-eslint-parser@9.4.2) + eslint-plugin-vue: 9.21.1(eslint@9.0.0) + eslint-plugin-vue-scoped-css: 2.7.2(eslint@9.0.0)(vue-eslint-parser@9.4.2) local-pkg: 0.5.0 prettier: 3.2.5 typescript: 5.4.5 - vue-eslint-parser: 9.4.2(eslint@8.57.0) + vue-eslint-parser: 9.4.2(eslint@9.0.0) transitivePeerDependencies: - '@babel/core' - '@types/eslint' @@ -2303,23 +2454,6 @@ packages: - supports-color dev: true - /@eslint/eslintrc@2.1.4: - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4(supports-color@8.1.1) - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.2.4 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - /@eslint/eslintrc@3.0.2: resolution: {integrity: sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2342,11 +2476,6 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@eslint/js@8.57.0: - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /@eslint/js@9.0.0: resolution: {integrity: sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2363,17 +2492,6 @@ packages: - supports-color dev: true - /@humanwhocodes/config-array@0.11.14: - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.4(supports-color@8.1.1) - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - /@humanwhocodes/config-array@0.12.3: resolution: {integrity: sha512-jsNnTBlMWuTpDkeE3on7+dWJi0D6fdDfeANj/w7MpS8ztROCoLvIO2nG0CcFj+E4k8j4QrSTh4Oryi3i2G669g==} engines: {node: '>=10.10.0'} @@ -2576,7 +2694,7 @@ packages: cpu: [wasm32] requiresBuild: true dependencies: - '@emnapi/runtime': 1.1.0 + '@emnapi/runtime': 1.1.1 dev: false optional: true @@ -2852,14 +2970,33 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.20 + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + /@jridgewell/resolve-uri@3.1.1: resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} engines: {node: '>=6.0.0'} + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: true + /@jridgewell/set-array@1.1.2: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + dev: true + /@jridgewell/source-map@0.3.5: resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} dependencies: @@ -2881,6 +3018,13 @@ packages: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: @@ -3332,7 +3476,7 @@ packages: - encoding dev: false - /@rollup/plugin-alias@5.1.0(rollup@4.14.1): + /@rollup/plugin-alias@5.1.0(rollup@4.14.2): resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==} engines: {node: '>=14.0.0'} peerDependencies: @@ -3341,11 +3485,11 @@ packages: rollup: optional: true dependencies: - rollup: 4.14.1 + rollup: 4.14.2 slash: 4.0.0 dev: true - /@rollup/plugin-json@6.1.0(rollup@4.14.1): + /@rollup/plugin-json@6.1.0(rollup@4.14.2): resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -3354,8 +3498,8 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.14.1) - rollup: 4.14.1 + '@rollup/pluginutils': 5.1.0(rollup@4.14.2) + rollup: 4.14.2 dev: true /@rollup/pluginutils@4.2.1: @@ -3366,7 +3510,7 @@ packages: picomatch: 2.3.1 dev: true - /@rollup/pluginutils@5.1.0(rollup@4.14.1): + /@rollup/pluginutils@5.1.0(rollup@4.14.2): resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} engines: {node: '>=14.0.0'} peerDependencies: @@ -3378,123 +3522,123 @@ packages: '@types/estree': 1.0.3 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 4.14.1 + rollup: 4.14.2 dev: true - /@rollup/rollup-android-arm-eabi@4.14.1: - resolution: {integrity: sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==} + /@rollup/rollup-android-arm-eabi@4.14.2: + resolution: {integrity: sha512-ahxSgCkAEk+P/AVO0vYr7DxOD3CwAQrT0Go9BJyGQ9Ef0QxVOfjDZMiF4Y2s3mLyPrjonchIMH/tbWHucJMykQ==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.14.1: - resolution: {integrity: sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==} + /@rollup/rollup-android-arm64@4.14.2: + resolution: {integrity: sha512-lAarIdxZWbFSHFSDao9+I/F5jDaKyCqAPMq5HqnfpBw8dKDiCaaqM0lq5h1pQTLeIqueeay4PieGR5jGZMWprw==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.14.1: - resolution: {integrity: sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==} + /@rollup/rollup-darwin-arm64@4.14.2: + resolution: {integrity: sha512-SWsr8zEUk82KSqquIMgZEg2GE5mCSfr9sE/thDROkX6pb3QQWPp8Vw8zOq2GyxZ2t0XoSIUlvHDkrf5Gmf7x3Q==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.14.1: - resolution: {integrity: sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==} + /@rollup/rollup-darwin-x64@4.14.2: + resolution: {integrity: sha512-o/HAIrQq0jIxJAhgtIvV5FWviYK4WB0WwV91SLUnsliw1lSAoLsmgEEgRWzDguAFeUEUUoIWXiJrPqU7vGiVkA==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.14.1: - resolution: {integrity: sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==} + /@rollup/rollup-linux-arm-gnueabihf@4.14.2: + resolution: {integrity: sha512-nwlJ65UY9eGq91cBi6VyDfArUJSKOYt5dJQBq8xyLhvS23qO+4Nr/RreibFHjP6t+5ap2ohZrUJcHv5zk5ju/g==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.14.1: - resolution: {integrity: sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==} + /@rollup/rollup-linux-arm64-gnu@4.14.2: + resolution: {integrity: sha512-Pg5TxxO2IVlMj79+c/9G0LREC9SY3HM+pfAwX7zj5/cAuwrbfj2Wv9JbMHIdPCfQpYsI4g9mE+2Bw/3aeSs2rQ==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.14.1: - resolution: {integrity: sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==} + /@rollup/rollup-linux-arm64-musl@4.14.2: + resolution: {integrity: sha512-cAOTjGNm84gc6tS02D1EXtG7tDRsVSDTBVXOLbj31DkwfZwgTPYZ6aafSU7rD/4R2a34JOwlF9fQayuTSkoclA==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-powerpc64le-gnu@4.14.1: - resolution: {integrity: sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==} - cpu: [ppc64le] + /@rollup/rollup-linux-powerpc64le-gnu@4.14.2: + resolution: {integrity: sha512-4RyT6v1kXb7C0fn6zV33rvaX05P0zHoNzaXI/5oFHklfKm602j+N4mn2YvoezQViRLPnxP8M1NaY4s/5kXO5cw==} + cpu: [ppc64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-riscv64-gnu@4.14.1: - resolution: {integrity: sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==} + /@rollup/rollup-linux-riscv64-gnu@4.14.2: + resolution: {integrity: sha512-KNUH6jC/vRGAKSorySTyc/yRYlCwN/5pnMjXylfBniwtJx5O7X17KG/0efj8XM3TZU7raYRXJFFReOzNmL1n1w==} cpu: [riscv64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-s390x-gnu@4.14.1: - resolution: {integrity: sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==} + /@rollup/rollup-linux-s390x-gnu@4.14.2: + resolution: {integrity: sha512-xPV4y73IBEXToNPa3h5lbgXOi/v0NcvKxU0xejiFw6DtIYQqOTMhZ2DN18/HrrP0PmiL3rGtRG9gz1QE8vFKXQ==} cpu: [s390x] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.14.1: - resolution: {integrity: sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==} + /@rollup/rollup-linux-x64-gnu@4.14.2: + resolution: {integrity: sha512-QBhtr07iFGmF9egrPOWyO5wciwgtzKkYPNLVCFZTmr4TWmY0oY2Dm/bmhHjKRwZoGiaKdNcKhFtUMBKvlchH+Q==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.14.1: - resolution: {integrity: sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==} + /@rollup/rollup-linux-x64-musl@4.14.2: + resolution: {integrity: sha512-8zfsQRQGH23O6qazZSFY5jP5gt4cFvRuKTpuBsC1ZnSWxV8ZKQpPqOZIUtdfMOugCcBvFGRa1pDC/tkf19EgBw==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.14.1: - resolution: {integrity: sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==} + /@rollup/rollup-win32-arm64-msvc@4.14.2: + resolution: {integrity: sha512-H4s8UjgkPnlChl6JF5empNvFHp77Jx+Wfy2EtmYPe9G22XV+PMuCinZVHurNe8ggtwoaohxARJZbaH/3xjB/FA==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.14.1: - resolution: {integrity: sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==} + /@rollup/rollup-win32-ia32-msvc@4.14.2: + resolution: {integrity: sha512-djqpAjm/i8erWYF0K6UY4kRO3X5+T4TypIqw60Q8MTqSBaQNpNXDhxdjpZ3ikgb+wn99svA7jxcXpiyg9MUsdw==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.14.1: - resolution: {integrity: sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==} + /@rollup/rollup-win32-x64-msvc@4.14.2: + resolution: {integrity: sha512-teAqzLT0yTYZa8ZP7zhFKEx4cotS8Tkk5XiqNMJhD4CpaWB1BHARE4Qy+RzwnXvSAYv+Q3jAqCVBS+PS+Yee8Q==} cpu: [x64] os: [win32] requiresBuild: true @@ -4301,8 +4445,8 @@ packages: resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==} dev: true - /@types/sortablejs@1.15.4: - resolution: {integrity: sha512-7oL7CcPSfoyoNx3Ba1+79ykJzpEKVhHUyfAiN5eT/FoeDXOR3eBDLXf9ndDNuxaExmjpI+zVi2dMMuaoXUOzNA==} + /@types/sortablejs@1.15.8: + resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} dev: true /@types/stack-utils@2.0.2: @@ -4435,7 +4579,7 @@ packages: - supports-color dev: true - /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.5): + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(typescript@5.4.5): resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4447,12 +4591,12 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.9.1 - '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5) '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/type-utils': 5.62.0(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/utils': 5.62.0(eslint@9.0.0)(typescript@5.4.5) debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + eslint: 9.0.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare-lite: 1.4.0 @@ -4463,7 +4607,7 @@ packages: - supports-color dev: true - /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.5): + /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(typescript@5.4.5): resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -4475,13 +4619,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.9.1 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/type-utils': 6.21.0(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/utils': 6.21.0(eslint@9.0.0)(typescript@5.4.5) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + eslint: 9.0.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare: 1.4.0 @@ -4512,7 +4656,7 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5): + /@typescript-eslint/parser@5.62.0(eslint@9.0.0)(typescript@5.4.5): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4526,13 +4670,13 @@ packages: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + eslint: 9.0.0 typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5): + /@typescript-eslint/parser@6.21.0(eslint@9.0.0)(typescript@5.4.5): resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -4547,7 +4691,7 @@ packages: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + eslint: 9.0.0 typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -4589,7 +4733,7 @@ packages: - supports-color dev: true - /@typescript-eslint/type-utils@5.62.0(eslint@8.57.0)(typescript@5.4.5): + /@typescript-eslint/type-utils@5.62.0(eslint@9.0.0)(typescript@5.4.5): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4600,16 +4744,16 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) - '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/utils': 5.62.0(eslint@9.0.0)(typescript@5.4.5) debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + eslint: 9.0.0 tsutils: 3.21.0(typescript@5.4.5) typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.4.5): + /@typescript-eslint/type-utils@6.21.0(eslint@9.0.0)(typescript@5.4.5): resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -4620,9 +4764,9 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/utils': 6.21.0(eslint@9.0.0)(typescript@5.4.5) debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + eslint: 9.0.0 ts-api-utils: 1.0.1(typescript@5.4.5) typescript: 5.4.5 transitivePeerDependencies: @@ -4723,19 +4867,19 @@ packages: - typescript dev: true - /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.4.5): + /@typescript-eslint/utils@5.62.0(eslint@9.0.0)(typescript@5.4.5): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) '@types/json-schema': 7.0.12 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) - eslint: 8.57.0 + eslint: 9.0.0 eslint-scope: 5.1.1 semver: 7.6.0 transitivePeerDependencies: @@ -4743,19 +4887,19 @@ packages: - typescript dev: true - /@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.4.5): + /@typescript-eslint/utils@6.21.0(eslint@9.0.0)(typescript@5.4.5): resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) '@types/json-schema': 7.0.14 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5) - eslint: 8.57.0 + eslint: 9.0.0 semver: 7.6.0 transitivePeerDependencies: - supports-color @@ -4778,10 +4922,6 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - dev: true - /@vercel/nft@0.26.3: resolution: {integrity: sha512-h1z/NN9ppS4YOKwSgBoopJlhm7tS2Qb/9Ld1HXjDpvvTE7mY0xVD8nllXs+RihD9uTGJISOIMzp18Eg0EApaMA==} engines: {node: '>=16'} @@ -4811,26 +4951,26 @@ packages: vite: ^5.0.0 vue: ^3.2.25 dependencies: - vite: 5.2.8(@types/node@20.12.7)(sass@1.74.1) + vite: 5.2.8(@types/node@20.12.7)(sass@1.75.0) vue: 3.4.21(typescript@5.4.5) dev: true - /@volar/language-core@2.2.0-alpha.7: - resolution: {integrity: sha512-igpp+nTkyl8faVzRJMpSCeA4XlBJ5UVSyc/WGyksmUmP10YbfufbcQCFlxEXv2uMBV+a3L4JVCj+Vju+08FOSA==} + /@volar/language-core@2.2.0-alpha.8: + resolution: {integrity: sha512-Ew1Iw7/RIRNuDLn60fWJdOLApAlfTVPxbPiSLzc434PReC9kleYtaa//Wo2WlN1oiRqneW0pWQQV0CwYqaimLQ==} dependencies: - '@volar/source-map': 2.2.0-alpha.7 + '@volar/source-map': 2.2.0-alpha.8 dev: true - /@volar/source-map@2.2.0-alpha.7: - resolution: {integrity: sha512-iIZM2EovdEnr6mMwlsnt4ciix4xz7HSGHyUSviRaY5cii5PMXGHeUU9UDeb+xzLCx8kdk3L5J4z+ts50AhkYcg==} + /@volar/source-map@2.2.0-alpha.8: + resolution: {integrity: sha512-E1ZVmXFJ5DU4fWDcWHzi8OLqqReqIDwhXvIMhVdk6+VipfMVv4SkryXu7/rs4GA/GsebcRyJdaSkKBB3OAkIcA==} dependencies: muggle-string: 0.4.1 dev: true - /@volar/typescript@2.2.0-alpha.7: - resolution: {integrity: sha512-qy04/hx4UbW1BdPlzaxlH60D4plubcyqdbYM6Y5vZiascZxFowtd6vE39Td9FYzDxwcKgzb/Crvf/ABhdHnuBA==} + /@volar/typescript@2.2.0-alpha.8: + resolution: {integrity: sha512-RLbRDI+17CiayHZs9HhSzlH0FhLl/+XK6o2qoiw2o2GGKcyD1aDoY6AcMd44acYncTOrqoTNoY6LuCiRyiJiGg==} dependencies: - '@volar/language-core': 2.2.0-alpha.7 + '@volar/language-core': 2.2.0-alpha.8 path-browserify: 1.0.1 dev: true @@ -4880,19 +5020,19 @@ packages: '@vue/shared': 3.4.21 dev: true - /@vue/language-core@2.0.12(typescript@5.4.5): - resolution: {integrity: sha512-aIStDPt69SHOpiIckGTIIjEz/sXc6ZfCMS5uWYL1AcbcRMhzFCLZscGAVte1+ad+RRFepSpKBjGttyPcgKJ7ww==} + /@vue/language-core@2.0.13(typescript@5.4.5): + resolution: {integrity: sha512-oQgM+BM66SU5GKtUMLQSQN0bxHFkFpLSSAiY87wVziPaiNQZuKVDt/3yA7GB9PiQw0y/bTNL0bOc0jM/siYjKg==} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@volar/language-core': 2.2.0-alpha.7 + '@volar/language-core': 2.2.0-alpha.8 '@vue/compiler-dom': 3.4.21 '@vue/shared': 3.4.21 computeds: 0.0.1 - minimatch: 9.0.3 + minimatch: 9.0.4 path-browserify: 1.0.1 typescript: 5.4.5 vue-template-compiler: 2.7.16 @@ -5607,6 +5747,10 @@ packages: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} dev: false + /async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + dev: false + /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -5693,9 +5837,17 @@ packages: /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} + dev: true - /aws-sdk@2.1597.0: - resolution: {integrity: sha512-YvApP9p5a5TD870mvQRrcUyJz3nKFrtlnDLaA4yrmAaidMDGzdNJ+AZlW0+onRCB4llzKD4Hos56zea0ulR+zQ==} + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: false + + /aws-sdk@2.1599.0: + resolution: {integrity: sha512-jPb1LAN+s1TLTK+VR3TTJLr//sb3AhhT60Bm9jxB5G/fVeeRczXtBtixNpQ00gksQdkstILYLc9S6MuKMsksxA==} engines: {node: '>= 10.0.0'} requiresBuild: true dependencies: @@ -6022,6 +6174,17 @@ packages: node-releases: 2.0.13 update-browserslist-db: 1.0.13(browserslist@4.22.1) + /browserslist@4.23.0: + resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001609 + electron-to-chromium: 1.4.736 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.23.0) + dev: true + /bs-logger@0.2.6: resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} engines: {node: '>= 6'} @@ -6075,7 +6238,7 @@ packages: resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} dependencies: base64-js: 1.5.1 - ieee754: 1.2.1 + ieee754: 1.1.13 isarray: 1.0.0 dev: false @@ -6260,6 +6423,10 @@ packages: /caniuse-lite@1.0.30001551: resolution: {integrity: sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==} + /caniuse-lite@1.0.30001609: + resolution: {integrity: sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA==} + dev: true + /canonicalize@1.0.8: resolution: {integrity: sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==} dev: false @@ -6451,6 +6618,21 @@ packages: fsevents: 2.3.3 dev: true + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -6623,7 +6805,7 @@ packages: resolution: {integrity: sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ==} dependencies: inflation: 2.0.0 - qs: 6.12.0 + qs: 6.12.1 raw-body: 2.5.2 type-is: 1.6.18 dev: false @@ -6632,7 +6814,7 @@ packages: resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==} dependencies: inflation: 2.0.0 - qs: 6.12.0 + qs: 6.12.1 raw-body: 2.5.2 type-is: 1.6.18 dev: false @@ -6845,7 +7027,7 @@ packages: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} dev: true - /consolidate@0.16.0(ejs@3.1.9)(pug@3.0.2): + /consolidate@0.16.0(ejs@3.1.10)(pug@3.0.2): resolution: {integrity: sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==} engines: {node: '>= 0.10.0'} deprecated: Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog @@ -7012,7 +7194,7 @@ packages: optional: true dependencies: bluebird: 3.7.2 - ejs: 3.1.9 + ejs: 3.1.10 pug: 3.0.2 dev: false @@ -7077,8 +7259,8 @@ packages: resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==} dev: false - /core-js@3.33.0: - resolution: {integrity: sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==} + /core-js@3.36.1: + resolution: {integrity: sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==} requiresBuild: true dev: true @@ -7477,8 +7659,8 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true - /deepl-node@1.12.0: - resolution: {integrity: sha512-c/8x1R0dXPL7NSDdQ94lYPou/A+I6cbo6b7gFb/28HbjcHnKB4RtWXWLgdv7n51GEXL7OE2eoRZQcAu4ZI+vGg==} + /deepl-node@1.13.0: + resolution: {integrity: sha512-pm8Al5B+/fRHiIKoreoSmv2RlXidF18+CznhtLILiYcj3EbxZpIhxWO8cgXCCsCTrUDMAbScIl8CuH3AqLPpGg==} engines: {node: '>=12.0'} dependencies: '@types/node': 20.12.7 @@ -7763,8 +7945,8 @@ packages: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false - /ejs@3.1.9: - resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} + /ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} hasBin: true dependencies: @@ -7778,6 +7960,10 @@ packages: /electron-to-chromium@1.4.561: resolution: {integrity: sha512-eS5t4ulWOBfVHdq9SW2dxEaFarj1lPjvJ8PaYMOjY0DecBaj/t4ARziL2IPpDr4atyWwjLFGQ2vo/VCgQFezVQ==} + /electron-to-chromium@1.4.736: + resolution: {integrity: sha512-Rer6wc3ynLelKNM4lOCg7/zPQj8tPOCB2hzD32PX9wd3hgRRi9MxEbmkFCokzcEhRVMiOVLjnL9ig9cefJ+6+Q==} + dev: true + /emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -8028,41 +8214,41 @@ packages: engines: {node: '>=12'} dev: true - /eslint-compat-utils@0.1.2(eslint@8.57.0): + /eslint-compat-utils@0.1.2(eslint@9.0.0): resolution: {integrity: sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==} engines: {node: '>=12'} peerDependencies: eslint: '>=6.0.0' dependencies: - eslint: 8.57.0 + eslint: 9.0.0 dev: true - /eslint-compat-utils@0.4.1(eslint@8.57.0): + /eslint-compat-utils@0.4.1(eslint@9.0.0): resolution: {integrity: sha512-5N7ZaJG5pZxUeNNJfUchurLVrunD1xJvyg5kYOIVF8kg1f3ajTikmAu/5fZ9w100omNPOoMjngRszh/Q/uFGMg==} engines: {node: '>=12'} peerDependencies: eslint: '>=6.0.0' dependencies: - eslint: 8.57.0 + eslint: 9.0.0 semver: 7.6.0 dev: true - /eslint-config-prettier@8.9.0(eslint@8.57.0): + /eslint-config-prettier@8.9.0(eslint@9.0.0): resolution: {integrity: sha512-+sbni7NfVXnOpnRadUA8S28AUlsZt9GjgFvABIRL9Hkn8KqNzOp+7Lw4QWtrwn20KzU3wqu1QoOj2m+7rKRqkA==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.57.0 + eslint: 9.0.0 dev: true - /eslint-config-prettier@9.1.0(eslint@8.57.0): + /eslint-config-prettier@9.1.0(eslint@9.0.0): resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.57.0 + eslint: 9.0.0 dev: true /eslint-config-standard@16.0.3(eslint-plugin-import@2.28.0)(eslint-plugin-node@11.1.0)(eslint-plugin-promise@6.1.1)(eslint@8.46.0): @@ -8132,7 +8318,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -8153,15 +8339,15 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5) debug: 3.2.7 - eslint: 8.57.0 + eslint: 9.0.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -8182,24 +8368,24 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5) debug: 3.2.7 - eslint: 8.57.0 + eslint: 9.0.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-es-x@7.5.0(eslint@8.57.0): + /eslint-plugin-es-x@7.5.0(eslint@9.0.0): resolution: {integrity: sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: eslint: '>=8' dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) '@eslint-community/regexpp': 4.9.1 - eslint: 8.57.0 - eslint-compat-utils: 0.1.2(eslint@8.57.0) + eslint: 9.0.0 + eslint-compat-utils: 0.1.2(eslint@9.0.0) dev: true /eslint-plugin-es@3.0.1(eslint@8.46.0): @@ -8213,35 +8399,35 @@ packages: regexpp: 3.2.0 dev: true - /eslint-plugin-es@4.1.0(eslint@8.57.0): + /eslint-plugin-es@4.1.0(eslint@9.0.0): resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} engines: {node: '>=8.10.0'} peerDependencies: eslint: '>=4.19.1' dependencies: - eslint: 8.57.0 + eslint: 9.0.0 eslint-utils: 2.1.0 regexpp: 3.2.0 dev: true - /eslint-plugin-eslint-comments@3.2.0(eslint@8.57.0): + /eslint-plugin-eslint-comments@3.2.0(eslint@9.0.0): resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} engines: {node: '>=6.5.0'} peerDependencies: eslint: '>=4.19.1' dependencies: escape-string-regexp: 1.0.5 - eslint: 8.57.0 + eslint: 9.0.0 ignore: 5.2.4 dev: true - /eslint-plugin-file-progress@1.3.0(eslint@8.57.0): + /eslint-plugin-file-progress@1.3.0(eslint@9.0.0): resolution: {integrity: sha512-LncpnGHU26KPvCrvDC2Sl9PfjdrsG8qltgiK6BR7KybWtfqrdlsu1ax3+hyPMn5OkKBTF3Wki3oqK1MSMeOtQw==} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: chalk: 4.1.2 - eslint: 8.57.0 + eslint: 9.0.0 ora: 5.4.1 dev: true @@ -8287,7 +8473,7 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0): + /eslint-plugin-import@2.29.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0): resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} engines: {node: '>=4'} peerDependencies: @@ -8297,16 +8483,16 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.57.0 + eslint: 9.0.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -8322,7 +8508,7 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.57.0): + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@9.0.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} peerDependencies: @@ -8332,16 +8518,16 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.57.0 + eslint: 9.0.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -8357,7 +8543,7 @@ packages: - supports-color dev: true - /eslint-plugin-jsdoc@48.0.6(eslint@8.57.0): + /eslint-plugin-jsdoc@48.0.6(eslint@9.0.0): resolution: {integrity: sha512-LgwXOX6TWxxFYcbdVe+BJ94Kl/pgjSPYHLzqEdAMXTA1BH9WDx7iJ+9/iDajPF64LtzWX8C1mCfpbMZjJGhAOw==} engines: {node: '>=18'} peerDependencies: @@ -8368,7 +8554,7 @@ packages: comment-parser: 1.4.1 debug: 4.3.4(supports-color@8.1.1) escape-string-regexp: 4.0.0 - eslint: 8.57.0 + eslint: 9.0.0 esquery: 1.5.0 is-builtin-module: 3.2.1 semver: 7.6.0 @@ -8377,28 +8563,28 @@ packages: - supports-color dev: true - /eslint-plugin-jsonc@2.10.0(eslint@8.57.0): + /eslint-plugin-jsonc@2.10.0(eslint@9.0.0): resolution: {integrity: sha512-9d//o6Jyh4s1RxC9fNSt1+MMaFN2ruFdXPG9XZcb/mR2KkfjADYiNL/hbU6W0Cyxfg3tS/XSFuhl5LgtMD8hmw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - eslint: 8.57.0 - eslint-compat-utils: 0.1.2(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) + eslint: 9.0.0 + eslint-compat-utils: 0.1.2(eslint@9.0.0) jsonc-eslint-parser: 2.3.0 natural-compare: 1.4.0 dev: true - /eslint-plugin-jsonc@2.13.0(eslint@8.57.0): + /eslint-plugin-jsonc@2.13.0(eslint@9.0.0): resolution: {integrity: sha512-2wWdJfpO/UbZzPDABuUVvlUQjfMJa2p2iQfYt/oWxOMpXCcjuiMUSaA02gtY/Dbu82vpaSqc+O7Xq6ECHwtIxA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - eslint: 8.57.0 - eslint-compat-utils: 0.4.1(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) + eslint: 9.0.0 + eslint-compat-utils: 0.4.1(eslint@9.0.0) espree: 9.6.1 graphemer: 1.4.0 jsonc-eslint-parser: 2.4.0 @@ -8406,28 +8592,28 @@ packages: synckit: 0.6.2 dev: true - /eslint-plugin-markdown@3.0.1(eslint@8.57.0): + /eslint-plugin-markdown@3.0.1(eslint@9.0.0): resolution: {integrity: sha512-8rqoc148DWdGdmYF6WSQFT3uQ6PO7zXYgeBpHAOAakX/zpq+NvFYbDA/H7PYzHajwtmaOzAwfxyl++x0g1/N9A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - eslint: 8.57.0 + eslint: 9.0.0 mdast-util-from-markdown: 0.8.5 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-n@15.7.0(eslint@8.57.0): + /eslint-plugin-n@15.7.0(eslint@9.0.0): resolution: {integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==} engines: {node: '>=12.22.0'} peerDependencies: eslint: '>=7.0.0' dependencies: builtins: 5.0.1 - eslint: 8.57.0 - eslint-plugin-es: 4.1.0(eslint@8.57.0) - eslint-utils: 3.0.0(eslint@8.57.0) + eslint: 9.0.0 + eslint-plugin-es: 4.1.0(eslint@9.0.0) + eslint-utils: 3.0.0(eslint@9.0.0) ignore: 5.2.4 is-core-module: 2.13.1 minimatch: 3.1.2 @@ -8435,16 +8621,16 @@ packages: semver: 7.6.0 dev: true - /eslint-plugin-n@16.6.2(eslint@8.57.0): + /eslint-plugin-n@16.6.2(eslint@9.0.0): resolution: {integrity: sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==} engines: {node: '>=16.0.0'} peerDependencies: eslint: '>=7.0.0' dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) builtins: 5.0.1 - eslint: 8.57.0 - eslint-plugin-es-x: 7.5.0(eslint@8.57.0) + eslint: 9.0.0 + eslint-plugin-es-x: 7.5.0(eslint@9.0.0) get-tsconfig: 4.7.2 globals: 13.24.0 ignore: 5.2.4 @@ -8470,7 +8656,7 @@ packages: semver: 6.3.1 dev: true - /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.9.0)(eslint@8.57.0)(prettier@3.2.5): + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5): resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -8481,13 +8667,13 @@ packages: eslint-config-prettier: optional: true dependencies: - eslint: 8.57.0 - eslint-config-prettier: 8.9.0(eslint@8.57.0) + eslint: 9.0.0 + eslint-config-prettier: 8.9.0(eslint@9.0.0) prettier: 3.2.5 prettier-linter-helpers: 1.0.0 dev: true - /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5): + /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5): resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -8501,20 +8687,20 @@ packages: eslint-config-prettier: optional: true dependencies: - eslint: 8.57.0 - eslint-config-prettier: 9.1.0(eslint@8.57.0) + eslint: 9.0.0 + eslint-config-prettier: 9.1.0(eslint@9.0.0) prettier: 3.2.5 prettier-linter-helpers: 1.0.0 synckit: 0.8.8 dev: true - /eslint-plugin-promise@5.2.0(eslint@8.57.0): + /eslint-plugin-promise@5.2.0(eslint@9.0.0): resolution: {integrity: sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: eslint: ^7.0.0 dependencies: - eslint: 8.57.0 + eslint: 9.0.0 dev: true /eslint-plugin-promise@6.1.1(eslint@8.46.0): @@ -8526,13 +8712,13 @@ packages: eslint: 8.46.0 dev: true - /eslint-plugin-promise@6.1.1(eslint@8.57.0): + /eslint-plugin-promise@6.1.1(eslint@9.0.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.57.0 + eslint: 9.0.0 dev: true /eslint-plugin-standard@5.0.0(eslint@8.46.0): @@ -8551,7 +8737,7 @@ packages: '@microsoft/tsdoc-config': 0.16.2 dev: true - /eslint-plugin-unicorn@40.1.0(eslint@8.57.0): + /eslint-plugin-unicorn@40.1.0(eslint@9.0.0): resolution: {integrity: sha512-y5doK2DF9Sr5AqKEHbHxjFllJ167nKDRU01HDcWyv4Tnmaoe9iNxMrBnaybZvWZUaE3OC5Unu0lNIevYamloig==} engines: {node: '>=12'} peerDependencies: @@ -8560,8 +8746,8 @@ packages: '@babel/helper-validator-identifier': 7.22.20 ci-info: 3.9.0 clean-regexp: 1.0.0 - eslint: 8.57.0 - eslint-utils: 3.0.0(eslint@8.57.0) + eslint: 9.0.0 + eslint-utils: 3.0.0(eslint@9.0.0) esquery: 1.5.0 indent-string: 4.0.0 is-builtin-module: 3.2.1 @@ -8574,17 +8760,17 @@ packages: strip-indent: 3.0.0 dev: true - /eslint-plugin-unicorn@45.0.2(eslint@8.57.0): + /eslint-plugin-unicorn@45.0.2(eslint@9.0.0): resolution: {integrity: sha512-Y0WUDXRyGDMcKLiwgL3zSMpHrXI00xmdyixEGIg90gHnj0PcHY4moNv3Ppje/kDivdAy5vUeUr7z211ImPv2gw==} engines: {node: '>=14.18'} peerDependencies: eslint: '>=8.28.0' dependencies: '@babel/helper-validator-identifier': 7.22.5 - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) ci-info: 3.9.0 clean-regexp: 1.0.0 - eslint: 8.57.0 + eslint: 9.0.0 esquery: 1.5.0 indent-string: 4.0.0 is-builtin-module: 3.2.1 @@ -8603,92 +8789,92 @@ packages: resolution: {integrity: sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==} dev: true - /eslint-plugin-vue-scoped-css@2.5.0(eslint@8.57.0)(vue-eslint-parser@9.3.1): + /eslint-plugin-vue-scoped-css@2.5.0(eslint@9.0.0)(vue-eslint-parser@9.3.1): resolution: {integrity: sha512-vR+raYNE1aQ69lS1lZGiKoz8rXFI3MWf2fxrfns/XCQ0XT5sIguhDtQS+9JmUQJClenLDEe2CQx7P+eeSdF4cA==} engines: {node: ^12.22 || ^14.17 || >=16} peerDependencies: eslint: '>=5.0.0' vue-eslint-parser: '>=7.1.0' dependencies: - eslint: 8.57.0 - eslint-utils: 3.0.0(eslint@8.57.0) + eslint: 9.0.0 + eslint-utils: 3.0.0(eslint@9.0.0) lodash: 4.17.21 postcss: 8.4.31 postcss-safe-parser: 6.0.0(postcss@8.4.31) postcss-scss: 4.0.6(postcss@8.4.31) postcss-selector-parser: 6.0.13 postcss-styl: 0.12.3 - vue-eslint-parser: 9.3.1(eslint@8.57.0) + vue-eslint-parser: 9.3.1(eslint@9.0.0) transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-vue-scoped-css@2.7.2(eslint@8.57.0)(vue-eslint-parser@9.4.2): + /eslint-plugin-vue-scoped-css@2.7.2(eslint@9.0.0)(vue-eslint-parser@9.4.2): resolution: {integrity: sha512-myJ99CJuwmAx5kq1WjgIeaUkxeU6PIEUh7age79Alm30bhN4fVTapOQLSMlvVTgxr36Y3igsZ3BCJM32LbHHig==} engines: {node: ^12.22 || ^14.17 || >=16} peerDependencies: eslint: '>=5.0.0' vue-eslint-parser: '>=7.1.0' dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - eslint: 8.57.0 - eslint-compat-utils: 0.4.1(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) + eslint: 9.0.0 + eslint-compat-utils: 0.4.1(eslint@9.0.0) lodash: 4.17.21 postcss: 8.4.31 postcss-safe-parser: 6.0.0(postcss@8.4.31) postcss-scss: 4.0.6(postcss@8.4.31) postcss-selector-parser: 6.0.13 postcss-styl: 0.12.3 - vue-eslint-parser: 9.4.2(eslint@8.57.0) + vue-eslint-parser: 9.4.2(eslint@9.0.0) transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-vue@9.16.1(eslint@8.57.0): + /eslint-plugin-vue@9.16.1(eslint@9.0.0): resolution: {integrity: sha512-2FtnTqazA6aYONfDuOZTk0QzwhAwi7Z4+uJ7+GHeGxcKapjqWlDsRWDenvyG/utyOfAS5bVRmAG3cEWiYEz2bA==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - eslint: 8.57.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) + eslint: 9.0.0 natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.0.13 semver: 7.6.0 - vue-eslint-parser: 9.3.2(eslint@8.57.0) + vue-eslint-parser: 9.3.2(eslint@9.0.0) xml-name-validator: 4.0.0 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-vue@9.21.1(eslint@8.57.0): + /eslint-plugin-vue@9.21.1(eslint@9.0.0): resolution: {integrity: sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - eslint: 8.57.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) + eslint: 9.0.0 natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.0.13 semver: 7.6.0 - vue-eslint-parser: 9.4.2(eslint@8.57.0) + vue-eslint-parser: 9.4.2(eslint@9.0.0) xml-name-validator: 4.0.0 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-yml@1.10.0(eslint@8.57.0): + /eslint-plugin-yml@1.10.0(eslint@9.0.0): resolution: {integrity: sha512-53SUwuNDna97lVk38hL/5++WXDuugPM9SUQ1T645R0EHMRCdBIIxGye/oOX2qO3FQ7aImxaUZJU/ju+NMUBrLQ==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 - eslint-compat-utils: 0.1.2(eslint@8.57.0) + eslint: 9.0.0 + eslint-compat-utils: 0.1.2(eslint@9.0.0) lodash: 4.17.21 natural-compare: 1.4.0 yaml-eslint-parser: 1.2.2 @@ -8696,15 +8882,15 @@ packages: - supports-color dev: true - /eslint-plugin-yml@1.12.2(eslint@8.57.0): + /eslint-plugin-yml@1.12.2(eslint@9.0.0): resolution: {integrity: sha512-hvS9p08FhPT7i/ynwl7/Wt7ke7Rf4P2D6fT8lZlL43peZDTsHtH2A0SIFQ7Kt7+mJ6if6P+FX3iJhMkdnxQwpg==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 - eslint-compat-utils: 0.4.1(eslint@8.57.0) + eslint: 9.0.0 + eslint-compat-utils: 0.4.1(eslint@9.0.0) lodash: 4.17.21 natural-compare: 1.4.0 yaml-eslint-parser: 1.2.2 @@ -8747,13 +8933,13 @@ packages: eslint-visitor-keys: 1.3.0 dev: true - /eslint-utils@3.0.0(eslint@8.57.0): + /eslint-utils@3.0.0(eslint@9.0.0): resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} peerDependencies: eslint: '>=5' dependencies: - eslint: 8.57.0 + eslint: 9.0.0 eslint-visitor-keys: 2.1.0 dev: true @@ -8828,53 +9014,6 @@ packages: - supports-color dev: true - /eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.9.1 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.5.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.2.4 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.3 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - dev: true - /eslint@9.0.0: resolution: {integrity: sha512-IMryZ5SudxzQvuod6rUdIUz29qFItWx281VhtFVc2Psy/ZhlCeD/5DT6lBIJ4H3G+iamGJoTln1v+QSuPw0p7Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -9584,7 +9723,7 @@ packages: dezalgo: 1.0.4 hexoid: 1.0.0 once: 1.4.0 - qs: 6.12.0 + qs: 6.12.1 dev: false /fragment-cache@0.2.1: @@ -9648,7 +9787,7 @@ packages: requiresBuild: true dependencies: bindings: 1.5.0 - nan: 2.18.0 + nan: 2.19.0 dev: false optional: true @@ -9727,9 +9866,9 @@ packages: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 - has-proto: 1.0.1 + has-proto: 1.0.3 has-symbols: 1.0.3 - hasown: 2.0.0 + hasown: 2.0.2 /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} @@ -10183,6 +10322,10 @@ packages: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} @@ -10193,6 +10336,12 @@ packages: dependencies: has-symbols: 1.0.3 + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + /has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} dev: true @@ -10240,6 +10389,12 @@ packages: dependencies: function-bind: 1.1.2 + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -10437,8 +10592,8 @@ packages: engines: {node: '>= 4'} dev: true - /immutable@4.3.2: - resolution: {integrity: sha512-oGXzbEDem9OOpDWZu88jGiYCvIsLHMvGw+8OXlpsvTFvIQplQbjg1B1cvKg8f7Hoch6+NGjpPsH1Fr+Mc2D1aA==} + /immutable@4.3.5: + resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==} dev: true /import-fresh@3.3.0: @@ -10595,7 +10750,7 @@ packages: resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} engines: {node: '>= 0.10'} dependencies: - hasown: 2.0.0 + hasown: 2.0.2 dev: false /is-alphabetical@1.0.4: @@ -10614,7 +10769,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.7 - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 dev: false /is-array-buffer@3.0.2: @@ -10657,7 +10812,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 dev: true /is-buffer@1.1.6: @@ -10689,14 +10844,14 @@ packages: resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} engines: {node: '>= 0.4'} dependencies: - hasown: 2.0.0 + hasown: 2.0.2 dev: false /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} dependencies: - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 dev: true /is-decimal@1.0.4: @@ -10829,7 +10984,7 @@ packages: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} dependencies: - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 dev: true /is-number@3.0.0: @@ -10947,6 +11102,14 @@ packages: engines: {node: '>= 0.4'} dependencies: which-typed-array: 1.1.11 + dev: true + + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.15 + dev: false /is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} @@ -11102,7 +11265,7 @@ packages: engines: {node: '>=10'} hasBin: true dependencies: - async: 3.2.4 + async: 3.2.5 chalk: 4.1.2 filelist: 1.0.4 minimatch: 3.1.2 @@ -12002,7 +12165,7 @@ packages: http-errors: 1.8.1 koa-compose: 4.1.0 methods: 1.1.2 - path-to-regexp: 6.2.1 + path-to-regexp: 6.2.2 transitivePeerDependencies: - supports-color dev: false @@ -12036,7 +12199,7 @@ packages: - supports-color dev: false - /koa-views@7.0.2(@types/koa@2.15.0)(ejs@3.1.9)(pug@3.0.2): + /koa-views@7.0.2(@types/koa@2.15.0)(ejs@3.1.10)(pug@3.0.2): resolution: {integrity: sha512-dvx3mdVeSVuIPEaKAoGbxLcenudvhl821xxyuRbcoA+bOJ2dvN8wlGjkLu0ZFMlkCscXZV6lzxy28rafeazI/w==} deprecated: This package is deprecated, please use the new fork @ladjs/koa-views. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/koa-views for updates and release changelog peerDependencies: @@ -12046,7 +12209,7 @@ packages: optional: true dependencies: '@types/koa': 2.15.0 - consolidate: 0.16.0(ejs@3.1.9)(pug@3.0.2) + consolidate: 0.16.0(ejs@3.1.10)(pug@3.0.2) debug: 4.3.4(supports-color@8.1.1) get-paths: 0.0.7 koa-send: 5.0.1 @@ -12275,8 +12438,8 @@ packages: engines: {node: '>=6.11.5'} dev: true - /loadjs@4.2.0: - resolution: {integrity: sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA==} + /loadjs@4.3.0: + resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==} dev: true /local-pkg@0.5.0: @@ -12727,6 +12890,13 @@ packages: dependencies: brace-expansion: 2.0.1 + /minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -12903,8 +13073,8 @@ packages: thenify-all: 1.6.0 dev: false - /nan@2.18.0: - resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==} + /nan@2.19.0: + resolution: {integrity: sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==} requiresBuild: true dev: false optional: true @@ -13055,6 +13225,10 @@ packages: /node-releases@2.0.13: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + dev: true + /nodemailer@6.9.13: resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==} engines: {node: '>=6.0.0'} @@ -13682,6 +13856,10 @@ packages: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} dev: false + /path-to-regexp@6.2.2: + resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + dev: false + /path-type@1.1.0: resolution: {integrity: sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==} engines: {node: '>=0.10.0'} @@ -13907,8 +14085,8 @@ packages: engines: {node: '>=14.19.0'} dev: false - /pnpm@8.15.6: - resolution: {integrity: sha512-d7iem+d6Kwatj0A6Gcrl4il29hAj+YrTI9XDAZSVjrwC7gpq5dE+5FT2E05OjK8poF8LGg4dKxe8prah8RWfhg==} + /pnpm@8.15.7: + resolution: {integrity: sha512-yFzSG22hAzIVaxyiqnnAph7nrS6wRTuIqymSienoypPmCRIyslwHy/YfbfdxKNnISeXJrG5EhU29IRxJ86Z63A==} engines: {node: '>=16.14'} hasBin: true dev: true @@ -13918,6 +14096,11 @@ packages: engines: {node: '>=0.10.0'} dev: false + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: false + /postcss-calc@5.3.1: resolution: {integrity: sha512-iBcptYFq+QUh9gzP7ta2btw50o40s4uLI4UDVgd5yRAZtUDWc5APdl5yQDd2h/TyiZNbJrv0HiYhT102CMgN7Q==} dependencies: @@ -14512,8 +14695,8 @@ packages: yargs: 15.4.1 dev: false - /qs@6.12.0: - resolution: {integrity: sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==} + /qs@6.12.1: + resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==} engines: {node: '>=0.6'} dependencies: side-channel: 1.0.6 @@ -15009,28 +15192,28 @@ packages: seedrandom: 2.4.2 dev: false - /rollup@4.14.1: - resolution: {integrity: sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==} + /rollup@4.14.2: + resolution: {integrity: sha512-WkeoTWvuBoFjFAhsEOHKRoZ3r9GfTyhh7Vff1zwebEFLEFjT1lG3784xEgKiTa7E+e70vsC81roVL2MP4tgEEQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.14.1 - '@rollup/rollup-android-arm64': 4.14.1 - '@rollup/rollup-darwin-arm64': 4.14.1 - '@rollup/rollup-darwin-x64': 4.14.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.14.1 - '@rollup/rollup-linux-arm64-gnu': 4.14.1 - '@rollup/rollup-linux-arm64-musl': 4.14.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.14.1 - '@rollup/rollup-linux-riscv64-gnu': 4.14.1 - '@rollup/rollup-linux-s390x-gnu': 4.14.1 - '@rollup/rollup-linux-x64-gnu': 4.14.1 - '@rollup/rollup-linux-x64-musl': 4.14.1 - '@rollup/rollup-win32-arm64-msvc': 4.14.1 - '@rollup/rollup-win32-ia32-msvc': 4.14.1 - '@rollup/rollup-win32-x64-msvc': 4.14.1 + '@rollup/rollup-android-arm-eabi': 4.14.2 + '@rollup/rollup-android-arm64': 4.14.2 + '@rollup/rollup-darwin-arm64': 4.14.2 + '@rollup/rollup-darwin-x64': 4.14.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.14.2 + '@rollup/rollup-linux-arm64-gnu': 4.14.2 + '@rollup/rollup-linux-arm64-musl': 4.14.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.14.2 + '@rollup/rollup-linux-riscv64-gnu': 4.14.2 + '@rollup/rollup-linux-s390x-gnu': 4.14.2 + '@rollup/rollup-linux-x64-gnu': 4.14.2 + '@rollup/rollup-linux-x64-musl': 4.14.2 + '@rollup/rollup-win32-arm64-msvc': 4.14.2 + '@rollup/rollup-win32-ia32-msvc': 4.14.2 + '@rollup/rollup-win32-x64-msvc': 4.14.2 fsevents: 2.3.3 dev: true @@ -15102,13 +15285,13 @@ packages: postcss: 8.4.35 dev: false - /sass@1.74.1: - resolution: {integrity: sha512-w0Z9p/rWZWelb88ISOLyvqTWGmtmu2QJICqDBGyNnfG4OUnPX9BBjjYIXUpXCMOOg5MQWNpqzt876la1fsTvUA==} + /sass@1.75.0: + resolution: {integrity: sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==} engines: {node: '>=14.0.0'} hasBin: true dependencies: - chokidar: 3.5.3 - immutable: 4.3.2 + chokidar: 3.6.0 + immutable: 4.3.5 source-map-js: 1.2.0 dev: true @@ -16257,7 +16440,7 @@ packages: typescript: 5.4.5 dev: true - /ts-jest@29.1.1(@babel/core@7.23.2)(jest@29.7.0)(typescript@4.9.4): + /ts-jest@29.1.1(@babel/core@7.24.4)(jest@29.7.0)(typescript@4.9.4): resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -16278,7 +16461,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 jest: 29.7.0(@types/node@18.11.18) @@ -16806,6 +16989,17 @@ packages: escalade: 3.1.1 picocolors: 1.0.0 + /update-browserslist-db@1.0.13(browserslist@4.23.0): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.23.0 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: @@ -16849,8 +17043,8 @@ packages: inherits: 2.0.4 is-arguments: 1.1.1 is-generator-function: 1.0.10 - is-typed-array: 1.1.12 - which-typed-array: 1.1.11 + is-typed-array: 1.1.13 + which-typed-array: 1.1.15 dev: false /uuid@8.0.0: @@ -16982,12 +17176,12 @@ packages: chalk: 4.1.2 debug: 4.3.4(supports-color@8.1.1) fs-extra: 10.1.0 - vite: 5.2.8(@types/node@20.12.7)(sass@1.74.1) + vite: 5.2.8(@types/node@20.12.7)(sass@1.75.0) transitivePeerDependencies: - supports-color dev: true - /vite@5.2.8(@types/node@20.12.7)(sass@1.74.1): + /vite@5.2.8(@types/node@20.12.7)(sass@1.75.0): resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -17018,8 +17212,8 @@ packages: '@types/node': 20.12.7 esbuild: 0.20.2 postcss: 8.4.38 - rollup: 4.14.1 - sass: 1.74.1 + rollup: 4.14.2 + sass: 1.75.0 optionalDependencies: fsevents: 2.3.3 dev: true @@ -17036,7 +17230,7 @@ packages: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} dev: true - /vue-draggable-plus@0.4.0(@types/sortablejs@1.15.4): + /vue-draggable-plus@0.4.0(@types/sortablejs@1.15.8): resolution: {integrity: sha512-CcvSopMmSZY9McCdQ56QsAydT5cZilx9LzLU0UIz6KDYe9ll6QnmNHN80t6wnxN402ZECSXc5X1dm/myiMFi+A==} peerDependencies: '@types/sortablejs': ^1.15.0 @@ -17045,17 +17239,17 @@ packages: '@vue/composition-api': optional: true dependencies: - '@types/sortablejs': 1.15.4 + '@types/sortablejs': 1.15.8 dev: true - /vue-eslint-parser@9.3.1(eslint@8.57.0): + /vue-eslint-parser@9.3.1(eslint@9.0.0): resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + eslint: 9.0.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 @@ -17066,14 +17260,14 @@ packages: - supports-color dev: true - /vue-eslint-parser@9.3.2(eslint@8.57.0): + /vue-eslint-parser@9.3.2(eslint@9.0.0): resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + eslint: 9.0.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 @@ -17084,14 +17278,14 @@ packages: - supports-color dev: true - /vue-eslint-parser@9.4.2(eslint@8.57.0): + /vue-eslint-parser@9.4.2(eslint@9.0.0): resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + eslint: 9.0.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 @@ -17125,14 +17319,14 @@ packages: he: 1.2.0 dev: true - /vue-tsc@2.0.12(typescript@5.4.5): - resolution: {integrity: sha512-thlBBWlPYrNdba535oDdxz7PRUufZgRZRVP5Aql5wBVpGSWSeqou4EzFXeKVoZr59lp9hJROubDVzlhACmcEhg==} + /vue-tsc@2.0.13(typescript@5.4.5): + resolution: {integrity: sha512-a3nL3FvguCWVJUQW/jFrUxdeUtiEkbZoQjidqvMeBK//tuE2w6NWQAbdrEpY2+6nSa4kZoKZp8TZUMtHpjt4mQ==} hasBin: true peerDependencies: typescript: '*' dependencies: - '@volar/typescript': 2.2.0-alpha.7 - '@vue/language-core': 2.0.12(typescript@5.4.5) + '@volar/typescript': 2.2.0-alpha.8 + '@vue/language-core': 2.0.13(typescript@5.4.5) semver: 7.6.0 typescript: 5.4.5 dev: true @@ -17319,6 +17513,18 @@ packages: for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 + dev: true + + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: false /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} @@ -17459,7 +17665,7 @@ packages: resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} dependencies: - sax: 1.2.4 + sax: 1.2.1 xmlbuilder: 11.0.1 dev: false @@ -17677,9 +17883,9 @@ packages: name: plyr version: 3.7.0 dependencies: - core-js: 3.33.0 + core-js: 3.36.1 custom-event-polyfill: 1.0.7 - loadjs: 4.2.0 + loadjs: 4.3.0 rangetouch: 2.0.1 url-polyfill: 1.1.12 dev: true From eb5dccacfe1a54c7d135c4a78e0df847573ec230 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Mon, 15 Apr 2024 04:17:35 +0900 Subject: [PATCH 060/110] chore (firefish-js): remove bun lockfile Why is it there? --- packages/firefish-js/bun.lockb | Bin 205415 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 packages/firefish-js/bun.lockb diff --git a/packages/firefish-js/bun.lockb b/packages/firefish-js/bun.lockb deleted file mode 100755 index 96788d2325e16f3b57147f261f42045089d2e48e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 205415 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 CdwLH5 From f412d7ace3fcbabf2cf007358e1ebafe2aecffed Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Mon, 15 Apr 2024 04:30:49 +0900 Subject: [PATCH 061/110] chore (backend): remove 'quiet' settings --- packages/backend/src/boot/index.ts | 4 +- packages/backend/src/boot/master.ts | 98 ++++++------ packages/backend/src/env.ts | 2 - .../remote/activitypub/kernel/delete/note.ts | 4 +- .../api/endpoints/notes/make-private.ts | 2 +- packages/backend/src/services/logger.ts | 1 - packages/backend/src/services/note/delete.ts | 142 +++++++++--------- 7 files changed, 119 insertions(+), 134 deletions(-) diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts index 8caf7d062e..4ba24c280b 100644 --- a/packages/backend/src/boot/index.ts +++ b/packages/backend/src/boot/index.ts @@ -76,9 +76,7 @@ cluster.on("exit", (worker) => { }); // Display detail of unhandled promise rejection -if (!envOption.quiet) { - process.on("unhandledRejection", console.dir); -} +process.on("unhandledRejection", console.dir); // Display detail of uncaught exception process.on("uncaughtException", (err) => { diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index e54a2889e2..cd21c33021 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -28,58 +28,56 @@ const bootLogger = logger.createSubLogger("boot", "magenta", false); const themeColor = chalk.hex("#31748f"); function greet() { - if (!envOption.quiet) { - //#region Firefish logo - console.log( - themeColor( - "██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗ ██╗ ○ ▄ ▄ ", - ), - ); - console.log( - themeColor( - "██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║ ██║ ⚬ █▄▄ █▄▄ ", - ), - ); - console.log( - themeColor( - "█████╗ ██║██████╔╝█████╗ █████╗ ██║███████╗███████║ ▄▄▄▄▄▄ ▄ ", - ), - ); - console.log( - themeColor( - "██╔══╝ ██║██╔══██╗██╔══╝ ██╔══╝ ██║╚════██║██╔══██║ █ █ █▄▄ ", - ), - ); - console.log( - themeColor( - "██║ ██║██║ ██║███████╗██║ ██║███████║██║ ██║ █ ● ● █ ", - ), - ); - console.log( - themeColor( - "╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ▀▄▄▄▄▄▄▀ ", - ), - ); - //#endregion + //#region Firefish logo + console.log( + themeColor( + "██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗ ██╗ ○ ▄ ▄ ", + ), + ); + console.log( + themeColor( + "██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║ ██║ ⚬ █▄▄ █▄▄ ", + ), + ); + console.log( + themeColor( + "█████╗ ██║██████╔╝█████╗ █████╗ ██║███████╗███████║ ▄▄▄▄▄▄ ▄ ", + ), + ); + console.log( + themeColor( + "██╔══╝ ██║██╔══██╗██╔══╝ ██╔══╝ ██║╚════██║██╔══██║ █ █ █▄▄ ", + ), + ); + console.log( + themeColor( + "██║ ██║██║ ██║███████╗██║ ██║███████║██║ ██║ █ ● ● █ ", + ), + ); + console.log( + themeColor( + "╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ▀▄▄▄▄▄▄▀ ", + ), + ); + //#endregion - console.log( - " Firefish is an open-source decentralized microblogging platform.", - ); - console.log( - chalk.rgb( - 255, - 136, - 0, - )( - " If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish", - ), - ); + console.log( + " Firefish is an open-source decentralized microblogging platform.", + ); + console.log( + chalk.rgb( + 255, + 136, + 0, + )( + " If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish", + ), + ); - console.log(""); - console.log( - chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`, - ); - } + console.log(""); + console.log( + chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`, + ); bootLogger.info("Welcome to Firefish!"); bootLogger.info(`Firefish v${meta.version}`, null, true); diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts index a788a0fba2..a10952133e 100644 --- a/packages/backend/src/env.ts +++ b/packages/backend/src/env.ts @@ -5,7 +5,6 @@ const envOption = { disableClustering: false, verbose: false, withLogTime: false, - quiet: false, slow: false, }; @@ -19,7 +18,6 @@ for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) { } if (process.env.NODE_ENV === "test") envOption.disableClustering = true; -if (process.env.NODE_ENV === "test") envOption.quiet = true; if (process.env.NODE_ENV === "test") envOption.noDaemons = true; export { envOption }; diff --git a/packages/backend/src/remote/activitypub/kernel/delete/note.ts b/packages/backend/src/remote/activitypub/kernel/delete/note.ts index 4656480c2f..ae3a593d05 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/note.ts @@ -1,5 +1,5 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import deleteNode from "@/services/note/delete.js"; +import deleteNote from "@/services/note/delete.js"; import { apLogger } from "../../logger.js"; import DbResolver from "../../db-resolver.js"; import { getApLock } from "@/misc/app-lock.js"; @@ -36,7 +36,7 @@ export default async function ( return "The user trying to delete the post is not the post author"; } - await deleteNode(actor, note); + await deleteNote(actor, note); return "ok: note deleted"; } finally { await lock.release(); diff --git a/packages/backend/src/server/api/endpoints/notes/make-private.ts b/packages/backend/src/server/api/endpoints/notes/make-private.ts index 7b9ebc4d1a..5ddf1f3bf1 100644 --- a/packages/backend/src/server/api/endpoints/notes/make-private.ts +++ b/packages/backend/src/server/api/endpoints/notes/make-private.ts @@ -53,7 +53,7 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.accessDenied); } - await deleteNote(user, note, false, false); + await deleteNote(user, note, false); await Notes.update(note.id, { visibility: "specified", visibleUserIds: [], diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts index 63eb3d00b9..47a1fe82f8 100644 --- a/packages/backend/src/services/logger.ts +++ b/packages/backend/src/services/logger.ts @@ -56,7 +56,6 @@ export default class Logger { subDomains: Domain[] = [], store = true, ): void { - if (envOption.quiet) return; if ( !(typeof config.logLevel === "undefined") && !config.logLevel.includes(level) diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index ac3515cfae..be3bf1e8b2 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -38,7 +38,6 @@ async function recalculateNotesCountOfLocalUser(user: { export default async function ( user: { id: User["id"]; uri: User["uri"]; host: User["host"] }, note: Note, - quiet = false, deleteFromDb = true, ) { const deletedAt = new Date(); @@ -67,87 +66,80 @@ export default async function ( } const instanceNotesCountDecreasement: Record<string, number> = {}; - if (!quiet) { - // Only broadcast "deleted" to local if the note is deleted from db + // Only broadcast "deleted" to local if the note is deleted from db + if (deleteFromDb) { + publishNoteStream(note.id, "deleted", { + deletedAt: deletedAt, + }); + } + + //#region ローカルの投稿なら削除アクティビティを配送 + if (Users.isLocalUser(user) && !note.localOnly) { + let renote: Note | null = null; + + // if deletd note is renote + if ( + note.renoteId && + note.text == null && + !note.hasPoll && + (note.fileIds == null || note.fileIds.length === 0) + ) { + renote = await Notes.findOneBy({ + id: note.renoteId, + }); + } + + const content = renderActivity( + renote + ? renderUndo( + renderAnnounce( + renote.uri || `${config.url}/notes/${renote.id}`, + note, + ), + user, + ) + : renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user), + ); + + deliverToConcerned(user, note, content); + } + + // also deliever delete activity to cascaded notes + for (const cascadingNote of cascadingNotes) { if (deleteFromDb) { - publishNoteStream(note.id, "deleted", { + // For other notes, publishNoteStream is also required. + publishNoteStream(cascadingNote.id, "deleted", { deletedAt: deletedAt, }); } - //#region ローカルの投稿なら削除アクティビティを配送 - if (Users.isLocalUser(user) && !note.localOnly) { - let renote: Note | null = null; - - // if deletd note is renote - if ( - note.renoteId && - note.text == null && - !note.hasPoll && - (note.fileIds == null || note.fileIds.length === 0) - ) { - renote = await Notes.findOneBy({ - id: note.renoteId, - }); - } - - const content = renderActivity( - renote - ? renderUndo( - renderAnnounce( - renote.uri || `${config.url}/notes/${renote.id}`, - note, - ), - user, - ) - : renderDelete( - renderTombstone(`${config.url}/notes/${note.id}`), - user, - ), - ); - - deliverToConcerned(user, note, content); + if (!cascadingNote.user) continue; + if (!Users.isLocalUser(cascadingNote.user)) { + if (!Users.isRemoteUser(cascadingNote.user)) continue; + instanceNotesCountDecreasement[cascadingNote.user.host] ??= 0; + instanceNotesCountDecreasement[cascadingNote.user.host]++; + continue; // filter out remote users } + affectedLocalUsers[cascadingNote.user.id] ??= cascadingNote.user; + if (cascadingNote.localOnly) continue; // filter out local-only notes + const content = renderActivity( + renderDelete( + renderTombstone(`${config.url}/notes/${cascadingNote.id}`), + cascadingNote.user, + ), + ); + deliverToConcerned(cascadingNote.user, cascadingNote, content); + } + //#endregion - // also deliever delete activity to cascaded notes - for (const cascadingNote of cascadingNotes) { - if (deleteFromDb) { - // For other notes, publishNoteStream is also required. - publishNoteStream(cascadingNote.id, "deleted", { - deletedAt: deletedAt, - }); - } - - if (!cascadingNote.user) continue; - if (!Users.isLocalUser(cascadingNote.user)) { - if (!Users.isRemoteUser(cascadingNote.user)) continue; - instanceNotesCountDecreasement[cascadingNote.user.host] ??= 0; - instanceNotesCountDecreasement[cascadingNote.user.host]++; - continue; // filter out remote users - } - affectedLocalUsers[cascadingNote.user.id] ??= cascadingNote.user; - if (cascadingNote.localOnly) continue; // filter out local-only notes - const content = renderActivity( - renderDelete( - renderTombstone(`${config.url}/notes/${cascadingNote.id}`), - cascadingNote.user, - ), - ); - deliverToConcerned(cascadingNote.user, cascadingNote, content); - } - //#endregion - - if (Users.isRemoteUser(user)) { - instanceNotesCountDecreasement[user.host] ??= 0; - instanceNotesCountDecreasement[user.host]++; - } - for (const [host, count] of Object.entries( - instanceNotesCountDecreasement, - )) { - registerOrFetchInstanceDoc(host).then((i) => { - Instances.decrement({ id: i.id }, "notesCount", count); - }); - } + if (Users.isRemoteUser(user)) { + instanceNotesCountDecreasement[user.host] ??= 0; + instanceNotesCountDecreasement[user.host]++; + } + for (const [host, count] of Object.entries(instanceNotesCountDecreasement)) { + registerOrFetchInstanceDoc(host).then((i) => { + Instances.decrement({ id: i.id }, "notesCount", count); + }); } if (deleteFromDb) { From 884c69f3777bfe26d5c519da102af450bcb6b65f Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Mon, 15 Apr 2024 04:34:00 +0900 Subject: [PATCH 062/110] chore (minor, backend): organize imports --- packages/backend/src/misc/reaction-lib.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts index 691d9743c8..fbdfe949ff 100644 --- a/packages/backend/src/misc/reaction-lib.ts +++ b/packages/backend/src/misc/reaction-lib.ts @@ -1,7 +1,6 @@ import { emojiRegex } from "./emoji-regex.js"; -import { fetchMeta } from "backend-rs"; +import { fetchMeta, toPuny } from "backend-rs"; import { Emojis } from "@/models/index.js"; -import { toPuny } from "backend-rs"; import { IsNull } from "typeorm"; export function convertReactions(reactions: Record<string, number>) { From 74875f174b1f84622ebb0375041483d2d04444e8 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Mon, 15 Apr 2024 04:34:36 +0900 Subject: [PATCH 063/110] chore (minor, backend): use a template literal --- packages/backend/src/misc/post.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/misc/post.ts b/packages/backend/src/misc/post.ts index dbe703d1a0..0b107ed009 100644 --- a/packages/backend/src/misc/post.ts +++ b/packages/backend/src/misc/post.ts @@ -12,7 +12,7 @@ export function parse(acct: any): Post { cw: acct.cw, localOnly: acct.localOnly, createdAt: new Date(acct.createdAt), - visibility: "hidden" + (acct.visibility || ""), + visibility: `hidden${acct.visibility || ""}`, }; } From 2731003bc948613e1161ea64219af6c361b75de1 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Mon, 15 Apr 2024 05:13:35 +0900 Subject: [PATCH 064/110] refactor (backend): port emoji-regex to backend-rs --- Cargo.lock | 34 ++ Cargo.toml | 1 + packages/backend-rs/Cargo.toml | 1 + packages/backend-rs/index.d.ts | 1 + packages/backend-rs/index.js | 3 +- packages/backend-rs/src/misc/emoji.rs | 31 ++ packages/backend-rs/src/misc/mod.rs | 1 + packages/backend/package.json | 1 - packages/backend/src/misc/emoji-regex.ts | 5 - packages/backend/src/misc/reaction-lib.ts | 13 +- .../server/api/mastodon/endpoints/status.ts | 5 +- pnpm-lock.yaml | 427 ++++++++---------- 12 files changed, 269 insertions(+), 254 deletions(-) create mode 100644 packages/backend-rs/src/misc/emoji.rs delete mode 100644 packages/backend/src/misc/emoji-regex.ts diff --git a/Cargo.lock b/Cargo.lock index bc584faf24..c6ba96e683 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,6 +209,7 @@ dependencies = [ "cfg-if", "chrono", "cuid2", + "emojis", "idna", "jsonschema", "macro_rs", @@ -702,6 +703,15 @@ dependencies = [ "serde", ] +[[package]] +name = "emojis" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee61eb945bff65ee7d19d157d39c67c33290ff0742907413fd5eefd29edc979" +dependencies = [ + "phf", +] + [[package]] name = "encoding_rs" version = "0.8.34" @@ -1662,6 +1672,24 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -2313,6 +2341,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index c9efe1f69a..7ca43c960b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ cfg-if = "1.0.0" chrono = "0.4.37" convert_case = "0.6.0" cuid2 = "0.1.2" +emojis = "0.6.1" idna = "0.5.0" jsonschema = "0.17.1" once_cell = "1.19.0" diff --git a/packages/backend-rs/Cargo.toml b/packages/backend-rs/Cargo.toml index 235fcc8706..af9e10cdc1 100644 --- a/packages/backend-rs/Cargo.toml +++ b/packages/backend-rs/Cargo.toml @@ -24,6 +24,7 @@ bcrypt = { workspace = true } cfg-if = { workspace = true } chrono = { workspace = true } cuid2 = { workspace = true } +emojis = { workspace = true } idna = { workspace = true } jsonschema = { workspace = true } once_cell = { workspace = true } diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index a9398aacc1..9b8a64142f 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -132,6 +132,7 @@ export function isSelfHost(host?: string | undefined | null): boolean export function isSameOrigin(uri: string): boolean export function extractHost(uri: string): string export function toPuny(host: string): string +export function isUnicodeEmoji(s: string): boolean export function sqlLikeEscape(src: string): string export function safeForSql(src: string): boolean /** Convert milliseconds to a human readable string */ diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 7a404d6447..16de16297f 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding module.exports.readServerConfig = readServerConfig module.exports.stringToAcct = stringToAcct @@ -321,6 +321,7 @@ module.exports.isSelfHost = isSelfHost module.exports.isSameOrigin = isSameOrigin module.exports.extractHost = extractHost module.exports.toPuny = toPuny +module.exports.isUnicodeEmoji = isUnicodeEmoji module.exports.sqlLikeEscape = sqlLikeEscape module.exports.safeForSql = safeForSql module.exports.formatMilliseconds = formatMilliseconds diff --git a/packages/backend-rs/src/misc/emoji.rs b/packages/backend-rs/src/misc/emoji.rs new file mode 100644 index 0000000000..df7d33848c --- /dev/null +++ b/packages/backend-rs/src/misc/emoji.rs @@ -0,0 +1,31 @@ +#[inline] +#[crate::export] +pub fn is_unicode_emoji(s: &str) -> bool { + emojis::get(s).is_some() +} + +#[cfg(test)] +mod unit_test { + use super::is_unicode_emoji; + + #[test] + fn test_unicode_emoji_check() { + assert!(is_unicode_emoji("⭐")); + assert!(is_unicode_emoji("👍")); + assert!(is_unicode_emoji("❤")); + assert!(is_unicode_emoji("♥️")); + assert!(is_unicode_emoji("❤️")); + assert!(is_unicode_emoji("💙")); + assert!(is_unicode_emoji("🩷")); + assert!(is_unicode_emoji("🖖🏿")); + assert!(is_unicode_emoji("🏃➡️")); + assert!(is_unicode_emoji("👩❤️👨")); + assert!(is_unicode_emoji("👩👦👦")); + assert!(is_unicode_emoji("🏳️🌈")); + + assert!(!is_unicode_emoji("⭐⭐")); + assert!(!is_unicode_emoji("x")); + assert!(!is_unicode_emoji("\t")); + assert!(!is_unicode_emoji(":meow_aww:")); + } +} diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index 74a483ea51..3fd447f4d5 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -1,6 +1,7 @@ pub mod acct; pub mod check_word_mute; pub mod convert_host; +pub mod emoji; pub mod escape_sql; pub mod format_milliseconds; pub mod mastodon_id; diff --git a/packages/backend/package.json b/packages/backend/package.json index bf435ec64b..9289c2f7ea 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -33,7 +33,6 @@ "@peertube/http-signature": "1.7.0", "@redocly/openapi-core": "1.11.0", "@sinonjs/fake-timers": "11.2.2", - "@twemoji/parser": "^15.1.1", "adm-zip": "0.5.10", "ajv": "8.12.0", "archiver": "7.0.1", diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts deleted file mode 100644 index 72d6a62d9a..0000000000 --- a/packages/backend/src/misc/emoji-regex.ts +++ /dev/null @@ -1,5 +0,0 @@ -import twemoji from "@twemoji/parser/dist/lib/regex.js"; -const twemojiRegex = twemoji.default; - -export const emojiRegex = new RegExp(`(${twemojiRegex.source})`); -export const emojiRegexAtStartToEnd = new RegExp(`^(${twemojiRegex.source})$`); diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts index fbdfe949ff..ac9ba5652d 100644 --- a/packages/backend/src/misc/reaction-lib.ts +++ b/packages/backend/src/misc/reaction-lib.ts @@ -1,5 +1,4 @@ -import { emojiRegex } from "./emoji-regex.js"; -import { fetchMeta, toPuny } from "backend-rs"; +import { fetchMeta, isUnicodeEmoji, toPuny } from "backend-rs"; import { Emojis } from "@/models/index.js"; import { IsNull } from "typeorm"; @@ -22,17 +21,15 @@ export async function toDbReaction( ): Promise<string> { if (!reaction) return (await fetchMeta(true)).defaultReaction; - reacterHost = reacterHost == null ? null : toPuny(reacterHost); - if (reaction.includes("❤") || reaction.includes("♥️")) return "❤️"; // Allow unicode reactions - const match = emojiRegex.exec(reaction); - if (match) { - const unicode = match[0]; - return unicode; + if (isUnicodeEmoji(reaction)) { + return reaction; } + reacterHost = reacterHost == null ? null : toPuny(reacterHost); + const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); if (custom) { const name = custom[1]; diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index 5286c90fac..6fa70717e7 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -1,10 +1,9 @@ import Router from "@koa/router"; import { getClient } from "../ApiMastodonCompatibleService.js"; -import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js"; import querystring from "node:querystring"; import qs from "qs"; import { convertTimelinesArgsId, limitToInt } from "./timeline.js"; -import { fetchMeta, fromMastodonId } from "backend-rs"; +import { fetchMeta, fromMastodonId, isUnicodeEmoji } from "backend-rs"; import { convertAccount, convertAttachment, @@ -37,7 +36,7 @@ export function apiStatusMastodon(router: Router): void { } const text = body.status; const removed = text.replace(/@\S+/g, "").replace(/\s|/g, ""); - const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed); + const isDefaultEmoji = isUnicodeEmoji(removed); const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed); if ((body.in_reply_to_id && isDefaultEmoji) || isCustomEmoji) { const a = await client.createEmojiReaction( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53b6089f9c..b591ffe9c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,9 +87,6 @@ importers: '@sinonjs/fake-timers': specifier: 11.2.2 version: 11.2.2 - '@twemoji/parser': - specifier: ^15.1.1 - version: 15.1.1 adm-zip: specifier: 0.5.10 version: 0.5.10 @@ -547,10 +544,10 @@ importers: devDependencies: '@eslint-sets/eslint-config-vue3': specifier: ^5.12.0 - version: 5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5) + version: 5.12.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5) '@eslint-sets/eslint-config-vue3-ts': specifier: ^3.3.0 - version: 3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5) + version: 3.3.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5) '@phosphor-icons/web': specifier: ^2.1.1 version: 2.1.1 @@ -658,7 +655,7 @@ importers: version: 3.0.12 eslint-plugin-file-progress: specifier: ^1.3.0 - version: 1.3.0(eslint@9.0.0) + version: 1.3.0(eslint@8.46.0) eventemitter3: specifier: 5.0.1 version: 5.0.1 @@ -1081,7 +1078,7 @@ packages: - supports-color dev: true - /@babel/eslint-parser@7.23.10(@babel/core@7.24.4)(eslint@9.0.0): + /@babel/eslint-parser@7.23.10(@babel/core@7.24.4)(eslint@8.46.0): resolution: {integrity: sha512-3wSYDPZVnhseRnxRJH6ZVTNknBz76AEnyC+AYYhasjP3Yy23qz0ERR7Fcd2SHmYuSFJ2kY9gaaDd3vyqU09eSw==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: @@ -1090,12 +1087,12 @@ packages: dependencies: '@babel/core': 7.24.4 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 9.0.0 + eslint: 8.46.0 eslint-visitor-keys: 2.1.0 semver: 6.3.1 dev: true - /@babel/eslint-parser@7.23.3(@babel/core@7.24.4)(eslint@9.0.0): + /@babel/eslint-parser@7.23.3(@babel/core@7.24.4)(eslint@8.46.0): resolution: {integrity: sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: @@ -1104,7 +1101,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 9.0.0 + eslint: 8.46.0 eslint-visitor-keys: 2.1.0 semver: 6.3.1 dev: true @@ -2256,29 +2253,29 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true - /@eslint-sets/eslint-config-basic@3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(prettier@3.2.5): + /@eslint-sets/eslint-config-basic@3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(prettier@3.2.5): resolution: {integrity: sha512-x5YH0CvZJxn19/5ehu188XaoLQpxOGlFiIuPHCN6FyONgrmriakT/cmIIBOJg2Vi/y1bn2xbhsgVNb00J3HyTg==} peerDependencies: eslint: '>=8.0.0' prettier: '>=2.0.0' dependencies: - '@babel/eslint-parser': 7.23.3(@babel/core@7.24.4)(eslint@9.0.0) - eslint: 9.0.0 - eslint-config-prettier: 8.9.0(eslint@9.0.0) - eslint-plugin-eslint-comments: 3.2.0(eslint@9.0.0) + '@babel/eslint-parser': 7.23.3(@babel/core@7.24.4)(eslint@8.46.0) + eslint: 8.46.0 + eslint-config-prettier: 8.9.0(eslint@8.46.0) + eslint-plugin-eslint-comments: 3.2.0(eslint@8.46.0) eslint-plugin-html: 7.1.0 - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0) - eslint-plugin-jsonc: 2.10.0(eslint@9.0.0) - eslint-plugin-markdown: 3.0.1(eslint@9.0.0) - eslint-plugin-n: 15.7.0(eslint@9.0.0) - eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5) - eslint-plugin-promise: 5.2.0(eslint@9.0.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0) + eslint-plugin-jsonc: 2.10.0(eslint@8.46.0) + eslint-plugin-markdown: 3.0.1(eslint@8.46.0) + eslint-plugin-n: 15.7.0(eslint@8.46.0) + eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.2.5) + eslint-plugin-promise: 5.2.0(eslint@8.46.0) eslint-plugin-tsdoc: 0.2.17 - eslint-plugin-unicorn: 45.0.2(eslint@9.0.0) - eslint-plugin-yml: 1.10.0(eslint@9.0.0) + eslint-plugin-unicorn: 45.0.2(eslint@8.46.0) + eslint-plugin-yml: 1.10.0(eslint@8.46.0) jsonc-eslint-parser: 2.3.0 prettier: 3.2.5 - vue-eslint-parser: 9.3.2(eslint@9.0.0) + vue-eslint-parser: 9.3.2(eslint@8.46.0) yaml-eslint-parser: 1.2.2 transitivePeerDependencies: - '@babel/core' @@ -2288,7 +2285,7 @@ packages: - supports-color dev: true - /@eslint-sets/eslint-config-basic@5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5): + /@eslint-sets/eslint-config-basic@5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5): resolution: {integrity: sha512-AgECfmJsiVOWKmvgjv780VuuoT9SE6PRgxGTtytHSfE9b9MAJjHxToVTKtD4UEKvocEGbg2EcwqGbff8cxDWKw==} peerDependencies: eslint: '>=7.4.0' @@ -2298,24 +2295,24 @@ packages: typescript: optional: true dependencies: - '@babel/eslint-parser': 7.23.10(@babel/core@7.24.4)(eslint@9.0.0) - eslint: 9.0.0 - eslint-config-prettier: 9.1.0(eslint@9.0.0) - eslint-plugin-eslint-comments: 3.2.0(eslint@9.0.0) + '@babel/eslint-parser': 7.23.10(@babel/core@7.24.4)(eslint@8.46.0) + eslint: 8.46.0 + eslint-config-prettier: 9.1.0(eslint@8.46.0) + eslint-plugin-eslint-comments: 3.2.0(eslint@8.46.0) eslint-plugin-html: 7.1.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@9.0.0) - eslint-plugin-jsonc: 2.13.0(eslint@9.0.0) - eslint-plugin-markdown: 3.0.1(eslint@9.0.0) - eslint-plugin-n: 16.6.2(eslint@9.0.0) - eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5) - eslint-plugin-promise: 6.1.1(eslint@9.0.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.46.0) + eslint-plugin-jsonc: 2.13.0(eslint@8.46.0) + eslint-plugin-markdown: 3.0.1(eslint@8.46.0) + eslint-plugin-n: 16.6.2(eslint@8.46.0) + eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.46.0)(prettier@3.2.5) + eslint-plugin-promise: 6.1.1(eslint@8.46.0) eslint-plugin-tsdoc: 0.2.17 - eslint-plugin-unicorn: 40.1.0(eslint@9.0.0) - eslint-plugin-yml: 1.12.2(eslint@9.0.0) + eslint-plugin-unicorn: 40.1.0(eslint@8.46.0) + eslint-plugin-yml: 1.12.2(eslint@8.46.0) jsonc-eslint-parser: 2.4.0 prettier: 3.2.5 typescript: 5.4.5 - vue-eslint-parser: 9.4.2(eslint@9.0.0) + vue-eslint-parser: 9.4.2(eslint@8.46.0) yaml-eslint-parser: 1.2.2 transitivePeerDependencies: - '@babel/core' @@ -2326,19 +2323,19 @@ packages: - supports-color dev: true - /@eslint-sets/eslint-config-ts@3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5): + /@eslint-sets/eslint-config-ts@3.3.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5): resolution: {integrity: sha512-4Vj3KxYx16hmW6AyEv1mil0gVN8H3rdJt8TRWufbAj0ZN+EjwOPf3TqE7ASCYto/NpA8xWQY3NGm/og9Or/dDQ==} peerDependencies: eslint: '>=8.0.0' prettier: '>=2.0.0' typescript: '>=4.0.0' dependencies: - '@eslint-sets/eslint-config-basic': 3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(prettier@3.2.5) - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(typescript@5.4.5) - '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5) - eslint: 9.0.0 - eslint-config-prettier: 8.9.0(eslint@9.0.0) - eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5) + '@eslint-sets/eslint-config-basic': 3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(prettier@3.2.5) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@5.4.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.4.5) + eslint: 8.46.0 + eslint-config-prettier: 8.9.0(eslint@8.46.0) + eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.2.5) eslint-plugin-tsdoc: 0.2.17 prettier: 3.2.5 typescript: 5.4.5 @@ -2349,7 +2346,7 @@ packages: - supports-color dev: true - /@eslint-sets/eslint-config-ts@5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5): + /@eslint-sets/eslint-config-ts@5.12.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5): resolution: {integrity: sha512-7vOzV6qYv0SbA9W17m9lkG/Zv+qVeCcAbWEY1d9hUbBHx9Ip48kNMNVDrnh97zUORXGcmjxsZ81W2lC36Ox2pw==} peerDependencies: eslint: '>=7.4.0' @@ -2359,12 +2356,12 @@ packages: typescript: optional: true dependencies: - '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5) - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(typescript@5.4.5) - '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5) - eslint: 9.0.0 - eslint-config-prettier: 9.1.0(eslint@9.0.0) - eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5) + '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(typescript@5.4.5) + '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5) + eslint: 8.46.0 + eslint-config-prettier: 9.1.0(eslint@8.46.0) + eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.46.0)(prettier@3.2.5) eslint-plugin-tsdoc: 0.2.17 prettier: 3.2.5 typescript: 5.4.5 @@ -2376,26 +2373,26 @@ packages: - supports-color dev: true - /@eslint-sets/eslint-config-vue3-ts@3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5): + /@eslint-sets/eslint-config-vue3-ts@3.3.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5): resolution: {integrity: sha512-KX3VFuS5U4FYKfZ6PABQjl54BMpNapNjYYe103Nm2Zy8y9zphDCBAARbhU97XNSvzkurve7HhJcsi9gXrWlGFA==} peerDependencies: eslint: '>=8.0.0' prettier: '>=2.0.0' typescript: '>=4.0.0' dependencies: - '@eslint-sets/eslint-config-ts': 3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5) - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(typescript@5.4.5) - '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5) - eslint: 9.0.0 - eslint-config-prettier: 8.9.0(eslint@9.0.0) - eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5) + '@eslint-sets/eslint-config-ts': 3.3.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@5.4.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.4.5) + eslint: 8.46.0 + eslint-config-prettier: 8.9.0(eslint@8.46.0) + eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.2.5) eslint-plugin-tsdoc: 0.2.17 eslint-plugin-vitest-globals: 1.4.0 - eslint-plugin-vue: 9.16.1(eslint@9.0.0) - eslint-plugin-vue-scoped-css: 2.5.0(eslint@9.0.0)(vue-eslint-parser@9.3.1) + eslint-plugin-vue: 9.16.1(eslint@8.46.0) + eslint-plugin-vue-scoped-css: 2.5.0(eslint@8.46.0)(vue-eslint-parser@9.3.1) prettier: 3.2.5 typescript: 5.4.5 - vue-eslint-parser: 9.3.1(eslint@9.0.0) + vue-eslint-parser: 9.3.1(eslint@8.46.0) transitivePeerDependencies: - '@babel/core' - eslint-import-resolver-typescript @@ -2403,7 +2400,7 @@ packages: - supports-color dev: true - /@eslint-sets/eslint-config-vue3@5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5): + /@eslint-sets/eslint-config-vue3@5.12.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5): resolution: {integrity: sha512-gQBmQicZihPcxncIdkKagQGZ2dH+97ioAlUpsaczEdgY9pLrLOU5oGTetjbaxAp6zGS2sXm1n0i2BnwRIlt4Bg==} peerDependencies: eslint: '>=7.4.0' @@ -2413,22 +2410,22 @@ packages: typescript: optional: true dependencies: - '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5) - '@eslint-sets/eslint-config-ts': 5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5) - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(typescript@5.4.5) - '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5) - eslint: 9.0.0 - eslint-config-prettier: 9.1.0(eslint@9.0.0) - eslint-plugin-jsdoc: 48.0.6(eslint@9.0.0) - eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5) + '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5) + '@eslint-sets/eslint-config-ts': 5.12.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(typescript@5.4.5) + '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5) + eslint: 8.46.0 + eslint-config-prettier: 9.1.0(eslint@8.46.0) + eslint-plugin-jsdoc: 48.0.6(eslint@8.46.0) + eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.46.0)(prettier@3.2.5) eslint-plugin-tsdoc: 0.2.17 eslint-plugin-vitest-globals: 1.4.0 - eslint-plugin-vue: 9.21.1(eslint@9.0.0) - eslint-plugin-vue-scoped-css: 2.7.2(eslint@9.0.0)(vue-eslint-parser@9.4.2) + eslint-plugin-vue: 9.21.1(eslint@8.46.0) + eslint-plugin-vue-scoped-css: 2.7.2(eslint@8.46.0)(vue-eslint-parser@9.4.2) local-pkg: 0.5.0 prettier: 3.2.5 typescript: 5.4.5 - vue-eslint-parser: 9.4.2(eslint@9.0.0) + vue-eslint-parser: 9.4.2(eslint@8.46.0) transitivePeerDependencies: - '@babel/core' - '@types/eslint' @@ -3880,10 +3877,6 @@ packages: /@twemoji/parser@15.0.0: resolution: {integrity: sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==} - /@twemoji/parser@15.1.1: - resolution: {integrity: sha512-CChRzIu6ngkCJOmURBlYEdX5DZSu+bBTtqR60XjBkFrmvplKW7OQsea+i8XwF4bLVlUXBO7ZmHhRPDzfQyLwwg==} - dev: false - /@types/accepts@1.3.5: resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==} dependencies: @@ -4579,7 +4572,7 @@ packages: - supports-color dev: true - /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(typescript@5.4.5): + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@5.4.5): resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4591,12 +4584,12 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.9.1 - '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.4.5) '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/type-utils': 5.62.0(eslint@9.0.0)(typescript@5.4.5) - '@typescript-eslint/utils': 5.62.0(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/type-utils': 5.62.0(eslint@8.46.0)(typescript@5.4.5) + '@typescript-eslint/utils': 5.62.0(eslint@8.46.0)(typescript@5.4.5) debug: 4.3.4(supports-color@8.1.1) - eslint: 9.0.0 + eslint: 8.46.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare-lite: 1.4.0 @@ -4607,7 +4600,7 @@ packages: - supports-color dev: true - /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(typescript@5.4.5): + /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(typescript@5.4.5): resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -4619,13 +4612,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.9.1 - '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@9.0.0)(typescript@5.4.5) - '@typescript-eslint/utils': 6.21.0(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/type-utils': 6.21.0(eslint@8.46.0)(typescript@5.4.5) + '@typescript-eslint/utils': 6.21.0(eslint@8.46.0)(typescript@5.4.5) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4(supports-color@8.1.1) - eslint: 9.0.0 + eslint: 8.46.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare: 1.4.0 @@ -4656,7 +4649,7 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@5.62.0(eslint@9.0.0)(typescript@5.4.5): + /@typescript-eslint/parser@5.62.0(eslint@8.46.0)(typescript@5.4.5): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4670,13 +4663,13 @@ packages: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) debug: 4.3.4(supports-color@8.1.1) - eslint: 9.0.0 + eslint: 8.46.0 typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@6.21.0(eslint@9.0.0)(typescript@5.4.5): + /@typescript-eslint/parser@6.21.0(eslint@8.46.0)(typescript@5.4.5): resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -4691,7 +4684,7 @@ packages: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4(supports-color@8.1.1) - eslint: 9.0.0 + eslint: 8.46.0 typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -4733,7 +4726,7 @@ packages: - supports-color dev: true - /@typescript-eslint/type-utils@5.62.0(eslint@9.0.0)(typescript@5.4.5): + /@typescript-eslint/type-utils@5.62.0(eslint@8.46.0)(typescript@5.4.5): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4744,16 +4737,16 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) - '@typescript-eslint/utils': 5.62.0(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/utils': 5.62.0(eslint@8.46.0)(typescript@5.4.5) debug: 4.3.4(supports-color@8.1.1) - eslint: 9.0.0 + eslint: 8.46.0 tsutils: 3.21.0(typescript@5.4.5) typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/type-utils@6.21.0(eslint@9.0.0)(typescript@5.4.5): + /@typescript-eslint/type-utils@6.21.0(eslint@8.46.0)(typescript@5.4.5): resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -4764,9 +4757,9 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5) - '@typescript-eslint/utils': 6.21.0(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/utils': 6.21.0(eslint@8.46.0)(typescript@5.4.5) debug: 4.3.4(supports-color@8.1.1) - eslint: 9.0.0 + eslint: 8.46.0 ts-api-utils: 1.0.1(typescript@5.4.5) typescript: 5.4.5 transitivePeerDependencies: @@ -4867,19 +4860,19 @@ packages: - typescript dev: true - /@typescript-eslint/utils@5.62.0(eslint@9.0.0)(typescript@5.4.5): + /@typescript-eslint/utils@5.62.0(eslint@8.46.0)(typescript@5.4.5): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) '@types/json-schema': 7.0.12 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) - eslint: 9.0.0 + eslint: 8.46.0 eslint-scope: 5.1.1 semver: 7.6.0 transitivePeerDependencies: @@ -4887,19 +4880,19 @@ packages: - typescript dev: true - /@typescript-eslint/utils@6.21.0(eslint@9.0.0)(typescript@5.4.5): + /@typescript-eslint/utils@6.21.0(eslint@8.46.0)(typescript@5.4.5): resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) '@types/json-schema': 7.0.14 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5) - eslint: 9.0.0 + eslint: 8.46.0 semver: 7.6.0 transitivePeerDependencies: - supports-color @@ -8214,41 +8207,41 @@ packages: engines: {node: '>=12'} dev: true - /eslint-compat-utils@0.1.2(eslint@9.0.0): + /eslint-compat-utils@0.1.2(eslint@8.46.0): resolution: {integrity: sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==} engines: {node: '>=12'} peerDependencies: eslint: '>=6.0.0' dependencies: - eslint: 9.0.0 + eslint: 8.46.0 dev: true - /eslint-compat-utils@0.4.1(eslint@9.0.0): + /eslint-compat-utils@0.4.1(eslint@8.46.0): resolution: {integrity: sha512-5N7ZaJG5pZxUeNNJfUchurLVrunD1xJvyg5kYOIVF8kg1f3ajTikmAu/5fZ9w100omNPOoMjngRszh/Q/uFGMg==} engines: {node: '>=12'} peerDependencies: eslint: '>=6.0.0' dependencies: - eslint: 9.0.0 + eslint: 8.46.0 semver: 7.6.0 dev: true - /eslint-config-prettier@8.9.0(eslint@9.0.0): + /eslint-config-prettier@8.9.0(eslint@8.46.0): resolution: {integrity: sha512-+sbni7NfVXnOpnRadUA8S28AUlsZt9GjgFvABIRL9Hkn8KqNzOp+7Lw4QWtrwn20KzU3wqu1QoOj2m+7rKRqkA==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 9.0.0 + eslint: 8.46.0 dev: true - /eslint-config-prettier@9.1.0(eslint@9.0.0): + /eslint-config-prettier@9.1.0(eslint@8.46.0): resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 9.0.0 + eslint: 8.46.0 dev: true /eslint-config-standard@16.0.3(eslint-plugin-import@2.28.0)(eslint-plugin-node@11.1.0)(eslint-plugin-promise@6.1.1)(eslint@8.46.0): @@ -8318,7 +8311,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.46.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -8339,53 +8332,24 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5) debug: 3.2.7 - eslint: 9.0.0 + eslint: 8.46.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.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': 6.21.0(eslint@9.0.0)(typescript@5.4.5) - debug: 3.2.7 - eslint: 9.0.0 - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint-plugin-es-x@7.5.0(eslint@9.0.0): + /eslint-plugin-es-x@7.5.0(eslint@8.46.0): resolution: {integrity: sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: eslint: '>=8' dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) '@eslint-community/regexpp': 4.9.1 - eslint: 9.0.0 - eslint-compat-utils: 0.1.2(eslint@9.0.0) + eslint: 8.46.0 + eslint-compat-utils: 0.1.2(eslint@8.46.0) dev: true /eslint-plugin-es@3.0.1(eslint@8.46.0): @@ -8399,35 +8363,35 @@ packages: regexpp: 3.2.0 dev: true - /eslint-plugin-es@4.1.0(eslint@9.0.0): + /eslint-plugin-es@4.1.0(eslint@8.46.0): resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} engines: {node: '>=8.10.0'} peerDependencies: eslint: '>=4.19.1' dependencies: - eslint: 9.0.0 + eslint: 8.46.0 eslint-utils: 2.1.0 regexpp: 3.2.0 dev: true - /eslint-plugin-eslint-comments@3.2.0(eslint@9.0.0): + /eslint-plugin-eslint-comments@3.2.0(eslint@8.46.0): resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} engines: {node: '>=6.5.0'} peerDependencies: eslint: '>=4.19.1' dependencies: escape-string-regexp: 1.0.5 - eslint: 9.0.0 + eslint: 8.46.0 ignore: 5.2.4 dev: true - /eslint-plugin-file-progress@1.3.0(eslint@9.0.0): + /eslint-plugin-file-progress@1.3.0(eslint@8.46.0): resolution: {integrity: sha512-LncpnGHU26KPvCrvDC2Sl9PfjdrsG8qltgiK6BR7KybWtfqrdlsu1ax3+hyPMn5OkKBTF3Wki3oqK1MSMeOtQw==} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: chalk: 4.1.2 - eslint: 9.0.0 + eslint: 8.46.0 ora: 5.4.1 dev: true @@ -8473,7 +8437,7 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.29.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0): + /eslint-plugin-import@2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0): resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} engines: {node: '>=4'} peerDependencies: @@ -8483,16 +8447,16 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.4.5) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.0.0 + eslint: 8.46.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.46.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -8508,7 +8472,7 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@9.0.0): + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.46.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} peerDependencies: @@ -8518,16 +8482,16 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5) + '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.0.0 + eslint: 8.46.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.46.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -8543,7 +8507,7 @@ packages: - supports-color dev: true - /eslint-plugin-jsdoc@48.0.6(eslint@9.0.0): + /eslint-plugin-jsdoc@48.0.6(eslint@8.46.0): resolution: {integrity: sha512-LgwXOX6TWxxFYcbdVe+BJ94Kl/pgjSPYHLzqEdAMXTA1BH9WDx7iJ+9/iDajPF64LtzWX8C1mCfpbMZjJGhAOw==} engines: {node: '>=18'} peerDependencies: @@ -8554,7 +8518,7 @@ packages: comment-parser: 1.4.1 debug: 4.3.4(supports-color@8.1.1) escape-string-regexp: 4.0.0 - eslint: 9.0.0 + eslint: 8.46.0 esquery: 1.5.0 is-builtin-module: 3.2.1 semver: 7.6.0 @@ -8563,28 +8527,28 @@ packages: - supports-color dev: true - /eslint-plugin-jsonc@2.10.0(eslint@9.0.0): + /eslint-plugin-jsonc@2.10.0(eslint@8.46.0): resolution: {integrity: sha512-9d//o6Jyh4s1RxC9fNSt1+MMaFN2ruFdXPG9XZcb/mR2KkfjADYiNL/hbU6W0Cyxfg3tS/XSFuhl5LgtMD8hmw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) - eslint: 9.0.0 - eslint-compat-utils: 0.1.2(eslint@9.0.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) + eslint: 8.46.0 + eslint-compat-utils: 0.1.2(eslint@8.46.0) jsonc-eslint-parser: 2.3.0 natural-compare: 1.4.0 dev: true - /eslint-plugin-jsonc@2.13.0(eslint@9.0.0): + /eslint-plugin-jsonc@2.13.0(eslint@8.46.0): resolution: {integrity: sha512-2wWdJfpO/UbZzPDABuUVvlUQjfMJa2p2iQfYt/oWxOMpXCcjuiMUSaA02gtY/Dbu82vpaSqc+O7Xq6ECHwtIxA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) - eslint: 9.0.0 - eslint-compat-utils: 0.4.1(eslint@9.0.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) + eslint: 8.46.0 + eslint-compat-utils: 0.4.1(eslint@8.46.0) espree: 9.6.1 graphemer: 1.4.0 jsonc-eslint-parser: 2.4.0 @@ -8592,28 +8556,28 @@ packages: synckit: 0.6.2 dev: true - /eslint-plugin-markdown@3.0.1(eslint@9.0.0): + /eslint-plugin-markdown@3.0.1(eslint@8.46.0): resolution: {integrity: sha512-8rqoc148DWdGdmYF6WSQFT3uQ6PO7zXYgeBpHAOAakX/zpq+NvFYbDA/H7PYzHajwtmaOzAwfxyl++x0g1/N9A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - eslint: 9.0.0 + eslint: 8.46.0 mdast-util-from-markdown: 0.8.5 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-n@15.7.0(eslint@9.0.0): + /eslint-plugin-n@15.7.0(eslint@8.46.0): resolution: {integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==} engines: {node: '>=12.22.0'} peerDependencies: eslint: '>=7.0.0' dependencies: builtins: 5.0.1 - eslint: 9.0.0 - eslint-plugin-es: 4.1.0(eslint@9.0.0) - eslint-utils: 3.0.0(eslint@9.0.0) + eslint: 8.46.0 + eslint-plugin-es: 4.1.0(eslint@8.46.0) + eslint-utils: 3.0.0(eslint@8.46.0) ignore: 5.2.4 is-core-module: 2.13.1 minimatch: 3.1.2 @@ -8621,16 +8585,16 @@ packages: semver: 7.6.0 dev: true - /eslint-plugin-n@16.6.2(eslint@9.0.0): + /eslint-plugin-n@16.6.2(eslint@8.46.0): resolution: {integrity: sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==} engines: {node: '>=16.0.0'} peerDependencies: eslint: '>=7.0.0' dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) builtins: 5.0.1 - eslint: 9.0.0 - eslint-plugin-es-x: 7.5.0(eslint@9.0.0) + eslint: 8.46.0 + eslint-plugin-es-x: 7.5.0(eslint@8.46.0) get-tsconfig: 4.7.2 globals: 13.24.0 ignore: 5.2.4 @@ -8656,7 +8620,7 @@ packages: semver: 6.3.1 dev: true - /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5): + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.2.5): resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -8667,13 +8631,13 @@ packages: eslint-config-prettier: optional: true dependencies: - eslint: 9.0.0 - eslint-config-prettier: 8.9.0(eslint@9.0.0) + eslint: 8.46.0 + eslint-config-prettier: 8.9.0(eslint@8.46.0) prettier: 3.2.5 prettier-linter-helpers: 1.0.0 dev: true - /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5): + /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.46.0)(prettier@3.2.5): resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -8687,20 +8651,20 @@ packages: eslint-config-prettier: optional: true dependencies: - eslint: 9.0.0 - eslint-config-prettier: 9.1.0(eslint@9.0.0) + eslint: 8.46.0 + eslint-config-prettier: 9.1.0(eslint@8.46.0) prettier: 3.2.5 prettier-linter-helpers: 1.0.0 synckit: 0.8.8 dev: true - /eslint-plugin-promise@5.2.0(eslint@9.0.0): + /eslint-plugin-promise@5.2.0(eslint@8.46.0): resolution: {integrity: sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: eslint: ^7.0.0 dependencies: - eslint: 9.0.0 + eslint: 8.46.0 dev: true /eslint-plugin-promise@6.1.1(eslint@8.46.0): @@ -8712,15 +8676,6 @@ packages: eslint: 8.46.0 dev: true - /eslint-plugin-promise@6.1.1(eslint@9.0.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: 9.0.0 - dev: true - /eslint-plugin-standard@5.0.0(eslint@8.46.0): resolution: {integrity: sha512-eSIXPc9wBM4BrniMzJRBm2uoVuXz2EPa+NXPk2+itrVt+r5SbKFERx/IgrK/HmfjddyKVz2f+j+7gBRvu19xLg==} deprecated: 'standard 16.0.0 and eslint-config-standard 16.0.0 no longer require the eslint-plugin-standard package. You can remove it from your dependencies with ''npm rm eslint-plugin-standard''. More info here: https://github.com/standard/standard/issues/1316' @@ -8737,7 +8692,7 @@ packages: '@microsoft/tsdoc-config': 0.16.2 dev: true - /eslint-plugin-unicorn@40.1.0(eslint@9.0.0): + /eslint-plugin-unicorn@40.1.0(eslint@8.46.0): resolution: {integrity: sha512-y5doK2DF9Sr5AqKEHbHxjFllJ167nKDRU01HDcWyv4Tnmaoe9iNxMrBnaybZvWZUaE3OC5Unu0lNIevYamloig==} engines: {node: '>=12'} peerDependencies: @@ -8746,8 +8701,8 @@ packages: '@babel/helper-validator-identifier': 7.22.20 ci-info: 3.9.0 clean-regexp: 1.0.0 - eslint: 9.0.0 - eslint-utils: 3.0.0(eslint@9.0.0) + eslint: 8.46.0 + eslint-utils: 3.0.0(eslint@8.46.0) esquery: 1.5.0 indent-string: 4.0.0 is-builtin-module: 3.2.1 @@ -8760,17 +8715,17 @@ packages: strip-indent: 3.0.0 dev: true - /eslint-plugin-unicorn@45.0.2(eslint@9.0.0): + /eslint-plugin-unicorn@45.0.2(eslint@8.46.0): resolution: {integrity: sha512-Y0WUDXRyGDMcKLiwgL3zSMpHrXI00xmdyixEGIg90gHnj0PcHY4moNv3Ppje/kDivdAy5vUeUr7z211ImPv2gw==} engines: {node: '>=14.18'} peerDependencies: eslint: '>=8.28.0' dependencies: '@babel/helper-validator-identifier': 7.22.5 - '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) ci-info: 3.9.0 clean-regexp: 1.0.0 - eslint: 9.0.0 + eslint: 8.46.0 esquery: 1.5.0 indent-string: 4.0.0 is-builtin-module: 3.2.1 @@ -8789,92 +8744,92 @@ packages: resolution: {integrity: sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==} dev: true - /eslint-plugin-vue-scoped-css@2.5.0(eslint@9.0.0)(vue-eslint-parser@9.3.1): + /eslint-plugin-vue-scoped-css@2.5.0(eslint@8.46.0)(vue-eslint-parser@9.3.1): resolution: {integrity: sha512-vR+raYNE1aQ69lS1lZGiKoz8rXFI3MWf2fxrfns/XCQ0XT5sIguhDtQS+9JmUQJClenLDEe2CQx7P+eeSdF4cA==} engines: {node: ^12.22 || ^14.17 || >=16} peerDependencies: eslint: '>=5.0.0' vue-eslint-parser: '>=7.1.0' dependencies: - eslint: 9.0.0 - eslint-utils: 3.0.0(eslint@9.0.0) + eslint: 8.46.0 + eslint-utils: 3.0.0(eslint@8.46.0) lodash: 4.17.21 postcss: 8.4.31 postcss-safe-parser: 6.0.0(postcss@8.4.31) postcss-scss: 4.0.6(postcss@8.4.31) postcss-selector-parser: 6.0.13 postcss-styl: 0.12.3 - vue-eslint-parser: 9.3.1(eslint@9.0.0) + vue-eslint-parser: 9.3.1(eslint@8.46.0) transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-vue-scoped-css@2.7.2(eslint@9.0.0)(vue-eslint-parser@9.4.2): + /eslint-plugin-vue-scoped-css@2.7.2(eslint@8.46.0)(vue-eslint-parser@9.4.2): resolution: {integrity: sha512-myJ99CJuwmAx5kq1WjgIeaUkxeU6PIEUh7age79Alm30bhN4fVTapOQLSMlvVTgxr36Y3igsZ3BCJM32LbHHig==} engines: {node: ^12.22 || ^14.17 || >=16} peerDependencies: eslint: '>=5.0.0' vue-eslint-parser: '>=7.1.0' dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) - eslint: 9.0.0 - eslint-compat-utils: 0.4.1(eslint@9.0.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) + eslint: 8.46.0 + eslint-compat-utils: 0.4.1(eslint@8.46.0) lodash: 4.17.21 postcss: 8.4.31 postcss-safe-parser: 6.0.0(postcss@8.4.31) postcss-scss: 4.0.6(postcss@8.4.31) postcss-selector-parser: 6.0.13 postcss-styl: 0.12.3 - vue-eslint-parser: 9.4.2(eslint@9.0.0) + vue-eslint-parser: 9.4.2(eslint@8.46.0) transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-vue@9.16.1(eslint@9.0.0): + /eslint-plugin-vue@9.16.1(eslint@8.46.0): resolution: {integrity: sha512-2FtnTqazA6aYONfDuOZTk0QzwhAwi7Z4+uJ7+GHeGxcKapjqWlDsRWDenvyG/utyOfAS5bVRmAG3cEWiYEz2bA==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) - eslint: 9.0.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) + eslint: 8.46.0 natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.0.13 semver: 7.6.0 - vue-eslint-parser: 9.3.2(eslint@9.0.0) + vue-eslint-parser: 9.3.2(eslint@8.46.0) xml-name-validator: 4.0.0 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-vue@9.21.1(eslint@9.0.0): + /eslint-plugin-vue@9.21.1(eslint@8.46.0): resolution: {integrity: sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) - eslint: 9.0.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) + eslint: 8.46.0 natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.0.13 semver: 7.6.0 - vue-eslint-parser: 9.4.2(eslint@9.0.0) + vue-eslint-parser: 9.4.2(eslint@8.46.0) xml-name-validator: 4.0.0 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-yml@1.10.0(eslint@9.0.0): + /eslint-plugin-yml@1.10.0(eslint@8.46.0): resolution: {integrity: sha512-53SUwuNDna97lVk38hL/5++WXDuugPM9SUQ1T645R0EHMRCdBIIxGye/oOX2qO3FQ7aImxaUZJU/ju+NMUBrLQ==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 9.0.0 - eslint-compat-utils: 0.1.2(eslint@9.0.0) + eslint: 8.46.0 + eslint-compat-utils: 0.1.2(eslint@8.46.0) lodash: 4.17.21 natural-compare: 1.4.0 yaml-eslint-parser: 1.2.2 @@ -8882,15 +8837,15 @@ packages: - supports-color dev: true - /eslint-plugin-yml@1.12.2(eslint@9.0.0): + /eslint-plugin-yml@1.12.2(eslint@8.46.0): resolution: {integrity: sha512-hvS9p08FhPT7i/ynwl7/Wt7ke7Rf4P2D6fT8lZlL43peZDTsHtH2A0SIFQ7Kt7+mJ6if6P+FX3iJhMkdnxQwpg==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 9.0.0 - eslint-compat-utils: 0.4.1(eslint@9.0.0) + eslint: 8.46.0 + eslint-compat-utils: 0.4.1(eslint@8.46.0) lodash: 4.17.21 natural-compare: 1.4.0 yaml-eslint-parser: 1.2.2 @@ -8933,13 +8888,13 @@ packages: eslint-visitor-keys: 1.3.0 dev: true - /eslint-utils@3.0.0(eslint@9.0.0): + /eslint-utils@3.0.0(eslint@8.46.0): resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} peerDependencies: eslint: '>=5' dependencies: - eslint: 9.0.0 + eslint: 8.46.0 eslint-visitor-keys: 2.1.0 dev: true @@ -17242,14 +17197,14 @@ packages: '@types/sortablejs': 1.15.8 dev: true - /vue-eslint-parser@9.3.1(eslint@9.0.0): + /vue-eslint-parser@9.3.1(eslint@8.46.0): resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 9.0.0 + eslint: 8.46.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 @@ -17260,14 +17215,14 @@ packages: - supports-color dev: true - /vue-eslint-parser@9.3.2(eslint@9.0.0): + /vue-eslint-parser@9.3.2(eslint@8.46.0): resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 9.0.0 + eslint: 8.46.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 @@ -17278,14 +17233,14 @@ packages: - supports-color dev: true - /vue-eslint-parser@9.4.2(eslint@9.0.0): + /vue-eslint-parser@9.4.2(eslint@8.46.0): resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 9.0.0 + eslint: 8.46.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 From 0f3126196f49b2eb57b08bc21929b3c115476d3a Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Mon, 15 Apr 2024 10:02:44 +0900 Subject: [PATCH 065/110] refactor (backend): port reaction-lib to backend-rs --- packages/backend-rs/index.d.ts | 8 + packages/backend-rs/index.js | 5 +- packages/backend-rs/src/misc/mod.rs | 1 + packages/backend-rs/src/misc/reaction.rs | 191 ++++++++++++++++++ packages/backend/src/misc/populate-emojis.ts | 3 +- packages/backend/src/misc/reaction-lib.ts | 83 -------- .../src/models/repositories/note-reaction.ts | 2 +- .../backend/src/models/repositories/note.ts | 5 +- .../src/services/note/reaction/create.ts | 5 +- .../src/services/note/reaction/delete.ts | 2 +- 10 files changed, 211 insertions(+), 94 deletions(-) create mode 100644 packages/backend-rs/src/misc/reaction.rs delete mode 100644 packages/backend/src/misc/reaction-lib.ts diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 9b8a64142f..a47495508e 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -156,6 +156,14 @@ export function nyaify(text: string, lang?: string | undefined | null): string export function hashPassword(password: string): string export function verifyPassword(password: string, hash: string): boolean export function isOldPasswordAlgorithm(hash: string): boolean +export interface DecodedReaction { + reaction: string + name: string | null + host: string | null +} +export function decodeReaction(reaction: string): DecodedReaction +export function countReactions(reactions: Record<string, number>): Record<string, number> +export function toDbReaction(reaction?: string | undefined | null, host?: string | undefined | null): Promise<string> export interface AbuseUserReport { id: string createdAt: Date diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 16de16297f..6fca851430 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding module.exports.readServerConfig = readServerConfig module.exports.stringToAcct = stringToAcct @@ -333,6 +333,9 @@ module.exports.nyaify = nyaify module.exports.hashPassword = hashPassword module.exports.verifyPassword = verifyPassword module.exports.isOldPasswordAlgorithm = isOldPasswordAlgorithm +module.exports.decodeReaction = decodeReaction +module.exports.countReactions = countReactions +module.exports.toDbReaction = toDbReaction module.exports.AntennaSrcEnum = AntennaSrcEnum module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum module.exports.NoteVisibilityEnum = NoteVisibilityEnum diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index 3fd447f4d5..45fd31cdcd 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -8,3 +8,4 @@ pub mod mastodon_id; pub mod meta; pub mod nyaify; pub mod password; +pub mod reaction; diff --git a/packages/backend-rs/src/misc/reaction.rs b/packages/backend-rs/src/misc/reaction.rs new file mode 100644 index 0000000000..a29ddf95de --- /dev/null +++ b/packages/backend-rs/src/misc/reaction.rs @@ -0,0 +1,191 @@ +use crate::database::db_conn; +use crate::misc::{convert_host::to_puny, emoji::is_unicode_emoji, meta::fetch_meta}; +use crate::model::entity::emoji; +use once_cell::sync::Lazy; +use regex::Regex; +use sea_orm::prelude::*; +use std::collections::HashMap; + +#[derive(PartialEq, Debug)] +#[crate::export(object)] +pub struct DecodedReaction { + pub reaction: String, + pub name: Option<String>, + pub host: Option<String>, +} + +#[crate::export] +pub fn decode_reaction(reaction: &str) -> DecodedReaction { + // Misskey allows you to include "+" and "-" in emoji shortcodes + // MFM spec: https://github.com/misskey-dev/mfm.js/blob/6aaf68089023c6adebe44123eebbc4dcd75955e0/docs/syntax.md?plain=1#L583 + // Misskey's implementation: https://github.com/misskey-dev/misskey/blob/bba3097765317cbf95d09627961b5b5dce16a972/packages/backend/src/core/ReactionService.ts#L68 + static RE: Lazy<Regex> = + Lazy::new(|| Regex::new(r"^:([0-9A-Za-z_+-]+)(?:@([0-9A-Za-z_.-]+))?:$").unwrap()); + + if let Some(captures) = RE.captures(reaction) { + let name = &captures[1]; + let host = captures.get(2).map(|s| s.as_str()); + + DecodedReaction { + reaction: format!(":{}@{}:", name, host.unwrap_or(".")), + name: Some(name.to_owned()), + host: host.map(|s| s.to_owned()), + } + } else { + DecodedReaction { + reaction: reaction.to_owned(), + name: None, + host: None, + } + } +} + +#[crate::export] +pub fn count_reactions(reactions: &HashMap<String, u32>) -> HashMap<String, u32> { + let mut res = HashMap::<String, u32>::new(); + + for (reaction, count) in reactions.iter() { + if count > &0 { + let decoded = decode_reaction(reaction).reaction; + let total = res.entry(decoded).or_insert(0); + *total += count; + } + } + + res +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Idna error: {0}")] + IdnaError(#[from] idna::Errors), + #[error("Database error: {0}")] + DbError(#[from] DbErr), +} + +#[crate::export] +pub async fn to_db_reaction(reaction: Option<&str>, host: Option<&str>) -> Result<String, Error> { + if let Some(reaction) = reaction { + // FIXME: Is it okay to do this only here? + // This was introduced in https://firefish.dev/firefish/firefish/-/commit/af730e75b6fc1a57ca680ce83459d7e433b130cf + if reaction.contains('❤') || reaction.contains("♥️") { + return Ok("❤️".to_owned()); + } + + if is_unicode_emoji(reaction) { + return Ok(reaction.to_owned()); + } + + static RE: Lazy<Regex> = + Lazy::new(|| Regex::new(r"^:([0-9A-Za-z_+-]+)(?:@\.)?:$").unwrap()); + + if let Some(captures) = RE.captures(reaction) { + let name = &captures[1]; + let db = db_conn().await?; + + if let Some(host) = host { + // remote emoji + let ascii_host = to_puny(host)?; + + // TODO: Does SeaORM have the `exists` method? + if emoji::Entity::find() + .filter(emoji::Column::Name.eq(name)) + .filter(emoji::Column::Host.eq(&ascii_host)) + .one(db) + .await? + .is_some() + { + return Ok(format!(":{name}@{ascii_host}:")); + } + } else { + // local emoji + // TODO: Does SeaORM have the `exists` method? + if emoji::Entity::find() + .filter(emoji::Column::Name.eq(name)) + .filter(emoji::Column::Host.is_null()) + .one(db) + .await? + .is_some() + { + return Ok(format!(":{name}:")); + } + } + }; + }; + + Ok(fetch_meta(true).await?.default_reaction) +} + +#[cfg(test)] +mod unit_test { + use super::{decode_reaction, DecodedReaction}; + use pretty_assertions::{assert_eq, assert_ne}; + + #[test] + fn test_decode_reaction() { + let unicode_emoji_1 = DecodedReaction { + reaction: "⭐".to_string(), + name: None, + host: None, + }; + let unicode_emoji_2 = DecodedReaction { + reaction: "🩷".to_string(), + name: None, + host: None, + }; + + assert_eq!(decode_reaction("⭐"), unicode_emoji_1); + assert_eq!(decode_reaction("🩷"), unicode_emoji_2); + + assert_ne!(decode_reaction("⭐"), unicode_emoji_2); + assert_ne!(decode_reaction("🩷"), unicode_emoji_1); + + let unicode_emoji_3 = DecodedReaction { + reaction: "🖖🏿".to_string(), + name: None, + host: None, + }; + assert_eq!(decode_reaction("🖖🏿"), unicode_emoji_3); + + let local_emoji = DecodedReaction { + reaction: ":meow_melt_tears@.:".to_string(), + name: Some("meow_melt_tears".to_string()), + host: None, + }; + assert_eq!(decode_reaction(":meow_melt_tears:"), local_emoji); + + let remote_emoji_1 = DecodedReaction { + reaction: ":meow_uwu@some-domain.example.org:".to_string(), + name: Some("meow_uwu".to_string()), + host: Some("some-domain.example.org".to_string()), + }; + assert_eq!( + decode_reaction(":meow_uwu@some-domain.example.org:"), + remote_emoji_1 + ); + + let remote_emoji_2 = DecodedReaction { + reaction: ":C++23@xn--eckwd4c7c.example.org:".to_string(), + name: Some("C++23".to_string()), + host: Some("xn--eckwd4c7c.example.org".to_string()), + }; + assert_eq!( + decode_reaction(":C++23@xn--eckwd4c7c.example.org:"), + remote_emoji_2 + ); + + let invalid_reaction_1 = DecodedReaction { + reaction: ":foo".to_string(), + name: None, + host: None, + }; + assert_eq!(decode_reaction(":foo"), invalid_reaction_1); + + let invalid_reaction_2 = DecodedReaction { + reaction: ":foo&@example.com:".to_string(), + name: None, + host: None, + }; + assert_eq!(decode_reaction(":foo&@example.com:"), invalid_reaction_2); + } +} diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts index f18b23c9a4..4ca60b222f 100644 --- a/packages/backend/src/misc/populate-emojis.ts +++ b/packages/backend/src/misc/populate-emojis.ts @@ -3,8 +3,7 @@ import { Emojis } from "@/models/index.js"; import type { Emoji } from "@/models/entities/emoji.js"; import type { Note } from "@/models/entities/note.js"; import { Cache } from "./cache.js"; -import { isSelfHost, toPuny } from "backend-rs"; -import { decodeReaction } from "./reaction-lib.js"; +import { decodeReaction, isSelfHost, toPuny } from "backend-rs"; import config from "@/config/index.js"; import { query } from "@/prelude/url.js"; import { redisClient } from "@/db/redis.js"; diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts deleted file mode 100644 index ac9ba5652d..0000000000 --- a/packages/backend/src/misc/reaction-lib.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { fetchMeta, isUnicodeEmoji, toPuny } from "backend-rs"; -import { Emojis } from "@/models/index.js"; -import { IsNull } from "typeorm"; - -export function convertReactions(reactions: Record<string, number>) { - const result = new Map(); - - for (const reaction in reactions) { - if (reactions[reaction] <= 0) continue; - - const decoded = decodeReaction(reaction).reaction; - result.set(decoded, (result.get(decoded) || 0) + reactions[reaction]); - } - - return Object.fromEntries(result); -} - -export async function toDbReaction( - reaction?: string | null, - reacterHost?: string | null, -): Promise<string> { - if (!reaction) return (await fetchMeta(true)).defaultReaction; - - if (reaction.includes("❤") || reaction.includes("♥️")) return "❤️"; - - // Allow unicode reactions - if (isUnicodeEmoji(reaction)) { - return reaction; - } - - reacterHost = reacterHost == null ? null : toPuny(reacterHost); - - const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); - if (custom) { - const name = custom[1]; - const emoji = await Emojis.findOneBy({ - host: reacterHost || IsNull(), - name, - }); - - if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; - } - - return (await fetchMeta(true)).defaultReaction; -} - -type DecodedReaction = { - /** - * リアクション名 (Unicode Emoji or ':name@hostname' or ':name@.') - */ - reaction: string; - - /** - * name (カスタム絵文字の場合name, Emojiクエリに使う) - */ - name?: string; - - /** - * host (カスタム絵文字の場合host, Emojiクエリに使う) - */ - host?: string | null; -}; - -export function decodeReaction(str: string): DecodedReaction { - const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/); - - if (custom) { - const name = custom[1]; - const host = custom[2] || null; - - return { - reaction: `:${name}@${host || "."}:`, // ローカル分は@以降を省略するのではなく.にする - name, - host, - }; - } - - return { - reaction: str, - name: undefined, - host: undefined, - }; -} diff --git a/packages/backend/src/models/repositories/note-reaction.ts b/packages/backend/src/models/repositories/note-reaction.ts index 20aae2876f..47e16ced0c 100644 --- a/packages/backend/src/models/repositories/note-reaction.ts +++ b/packages/backend/src/models/repositories/note-reaction.ts @@ -2,7 +2,7 @@ import { db } from "@/db/postgre.js"; import { NoteReaction } from "@/models/entities/note-reaction.js"; import { Notes, Users } from "../index.js"; import type { Packed } from "@/misc/schema.js"; -import { decodeReaction } from "@/misc/reaction-lib.js"; +import { decodeReaction } from "backend-rs"; import type { User } from "@/models/entities/user.js"; export const NoteReactionRepository = db.getRepository(NoteReaction).extend({ diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index ed6ecc4a5c..1bf4ef657f 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -12,9 +12,8 @@ import { Channels, } from "../index.js"; import type { Packed } from "@/misc/schema.js"; -import { nyaify } from "backend-rs"; +import { countReactions, decodeReaction, nyaify } from "backend-rs"; import { awaitAll } from "@/prelude/await-all.js"; -import { convertReactions, decodeReaction } from "@/misc/reaction-lib.js"; import type { NoteReaction } from "@/models/entities/note-reaction.js"; import { aggregateNoteEmojis, @@ -214,7 +213,7 @@ export const NoteRepository = db.getRepository(Note).extend({ note.visibility === "specified" ? note.visibleUserIds : undefined, renoteCount: note.renoteCount, repliesCount: note.repliesCount, - reactions: convertReactions(note.reactions), + reactions: countReactions(note.reactions), reactionEmojis: reactionEmoji, emojis: noteEmoji, tags: note.tags.length > 0 ? note.tags : undefined, diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts index a07ffdabad..3b8b97cefd 100644 --- a/packages/backend/src/services/note/reaction/create.ts +++ b/packages/backend/src/services/note/reaction/create.ts @@ -2,7 +2,6 @@ import { publishNoteStream } from "@/services/stream.js"; import { renderLike } from "@/remote/activitypub/renderer/like.js"; import DeliverManager from "@/remote/activitypub/deliver-manager.js"; import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { toDbReaction, decodeReaction } from "@/misc/reaction-lib.js"; import type { User, IRemoteUser } from "@/models/entities/user.js"; import type { Note } from "@/models/entities/note.js"; import { @@ -14,7 +13,7 @@ import { Blockings, } from "@/models/index.js"; import { IsNull, Not } from "typeorm"; -import { genId } from "backend-rs"; +import { decodeReaction, genId, toDbReaction } from "backend-rs"; import { createNotification } from "@/services/create-notification.js"; import deleteReaction from "./delete.js"; import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; @@ -95,7 +94,7 @@ export default async ( const emoji = await Emojis.findOne({ where: { - name: decodedReaction.name, + name: decodedReaction.name ?? undefined, host: decodedReaction.host ?? IsNull(), }, select: ["name", "host", "originalUrl", "publicUrl"], diff --git a/packages/backend/src/services/note/reaction/delete.ts b/packages/backend/src/services/note/reaction/delete.ts index 49879a0c02..e5416a78a8 100644 --- a/packages/backend/src/services/note/reaction/delete.ts +++ b/packages/backend/src/services/note/reaction/delete.ts @@ -7,7 +7,7 @@ import { IdentifiableError } from "@/misc/identifiable-error.js"; import type { User, IRemoteUser } from "@/models/entities/user.js"; import type { Note } from "@/models/entities/note.js"; import { NoteReactions, Users, Notes } from "@/models/index.js"; -import { decodeReaction } from "@/misc/reaction-lib.js"; +import { decodeReaction } from "backend-rs"; export default async ( user: { id: User["id"]; host: User["host"] }, From 71c158fbd3088ab3194e39aa8a6ce71cddde257d Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Mon, 15 Apr 2024 17:28:20 +0900 Subject: [PATCH 066/110] refactor (backend): port env.ts to backend-rs --- packages/backend-rs/index.d.ts | 10 +++++++ packages/backend-rs/index.js | 3 ++- packages/backend-rs/src/config/environment.rs | 27 +++++++++++++++++++ packages/backend-rs/src/config/mod.rs | 1 + packages/backend/src/boot/index.ts | 2 +- packages/backend/src/boot/master.ts | 2 +- packages/backend/src/config/index.ts | 2 ++ packages/backend/src/env.ts | 23 ---------------- packages/backend/src/queue/index.ts | 2 +- packages/backend/src/server/index.ts | 5 ++-- packages/backend/src/services/logger.ts | 3 +-- 11 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 packages/backend-rs/src/config/environment.rs delete mode 100644 packages/backend/src/env.ts diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index a47495508e..ae050e01d8 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -3,6 +3,16 @@ /* auto-generated by NAPI-RS */ +export interface EnvConfig { + onlyQueue: boolean + onlyServer: boolean + noDaemons: boolean + disableClustering: boolean + verbose: boolean + withLogTime: boolean + slow: boolean +} +export function readEnvironmentConfig(): EnvConfig export interface ServerConfig { url: string port: number diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 6fca851430..6acd3ca68d 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,8 +310,9 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +module.exports.readEnvironmentConfig = readEnvironmentConfig module.exports.readServerConfig = readServerConfig module.exports.stringToAcct = stringToAcct module.exports.acctToString = acctToString diff --git a/packages/backend-rs/src/config/environment.rs b/packages/backend-rs/src/config/environment.rs new file mode 100644 index 0000000000..7d66aec7ba --- /dev/null +++ b/packages/backend-rs/src/config/environment.rs @@ -0,0 +1,27 @@ +// FIXME: Are these options used? +#[crate::export(object)] +pub struct EnvConfig { + pub only_queue: bool, + pub only_server: bool, + pub no_daemons: bool, + pub disable_clustering: bool, + pub verbose: bool, + pub with_log_time: bool, + pub slow: bool, +} + +#[crate::export] +pub fn read_environment_config() -> EnvConfig { + let node_env = std::env::var("NODE_ENV").unwrap_or_default().to_lowercase(); + let is_testing = node_env == "test"; + + EnvConfig { + only_queue: std::env::var("MK_ONLY_QUEUE").is_ok(), + only_server: std::env::var("MK_ONLY_SERVER").is_ok(), + no_daemons: is_testing || std::env::var("MK_NO_DAEMONS").is_ok(), + disable_clustering: is_testing || std::env::var("MK_DISABLE_CLUSTERING").is_ok(), + verbose: std::env::var("MK_VERBOSE").is_ok(), + with_log_time: std::env::var("MK_WITH_LOG_TIME").is_ok(), + slow: std::env::var("MK_SLOW").is_ok(), + } +} diff --git a/packages/backend-rs/src/config/mod.rs b/packages/backend-rs/src/config/mod.rs index 74f47ad347..b708f2b265 100644 --- a/packages/backend-rs/src/config/mod.rs +++ b/packages/backend-rs/src/config/mod.rs @@ -1 +1,2 @@ +pub mod environment; pub mod server; diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts index 4ba24c280b..9854d2dce4 100644 --- a/packages/backend/src/boot/index.ts +++ b/packages/backend/src/boot/index.ts @@ -3,7 +3,7 @@ import chalk from "chalk"; import Xev from "xev"; import Logger from "@/services/logger.js"; -import { envOption } from "../env.js"; +import { envOption } from "@/config/index.js"; import { inspect } from "node:util"; // for typeorm diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index cd21c33021..3ba2d0cf50 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -10,7 +10,7 @@ import semver from "semver"; import Logger from "@/services/logger.js"; import loadConfig from "@/config/load.js"; import type { Config } from "@/config/types.js"; -import { envOption } from "@/env.js"; +import { envOption } from "@/config/index.js"; import { showMachineInfo } from "@/misc/show-machine-info.js"; import { db, initDb } from "@/db/postgre.js"; import { inspect } from "node:util"; diff --git a/packages/backend/src/config/index.ts b/packages/backend/src/config/index.ts index ae197b09ca..fe87e5026a 100644 --- a/packages/backend/src/config/index.ts +++ b/packages/backend/src/config/index.ts @@ -1,3 +1,5 @@ import load from "./load.js"; +import { readEnvironmentConfig } from "backend-rs"; export default load(); +export const envOption = readEnvironmentConfig(); diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts deleted file mode 100644 index a10952133e..0000000000 --- a/packages/backend/src/env.ts +++ /dev/null @@ -1,23 +0,0 @@ -const envOption = { - onlyQueue: false, - onlyServer: false, - noDaemons: false, - disableClustering: false, - verbose: false, - withLogTime: false, - slow: false, -}; - -for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) { - if ( - process.env[ - `MK_${key.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase()}` - ] - ) - envOption[key] = true; -} - -if (process.env.NODE_ENV === "test") envOption.disableClustering = true; -if (process.env.NODE_ENV === "test") envOption.noDaemons = true; - -export { envOption }; diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 58a0ae7486..e4e413be52 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -5,7 +5,7 @@ import config from "@/config/index.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; import type { IActivity } from "@/remote/activitypub/type.js"; import type { Webhook, webhookEventTypes } from "@/models/entities/webhook.js"; -import { envOption } from "../env.js"; +import { envOption } from "@/config/index.js"; import processDeliver from "./processors/deliver.js"; import processInbox from "./processors/inbox.js"; diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index 6cf837b4ed..17358a4758 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -13,15 +13,14 @@ import koaLogger from "koa-logger"; import * as slow from "koa-slow"; import { IsNull } from "typeorm"; -import config from "@/config/index.js"; +import config, { envOption } from "@/config/index.js"; import Logger from "@/services/logger.js"; import { Users } from "@/models/index.js"; import { fetchMeta } from "backend-rs"; import { genIdenticon } from "@/misc/gen-identicon.js"; import { createTemp } from "@/misc/create-temp.js"; import { stringToAcct } from "backend-rs"; -import { envOption } from "@/env.js"; -import megalodon, { MegalodonInterface } from "megalodon"; +import megalodon, { type MegalodonInterface } from "megalodon"; import activityPub from "./activitypub.js"; import nodeinfo from "./nodeinfo.js"; import wellKnown from "./well-known.js"; diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts index 47a1fe82f8..e53279e31c 100644 --- a/packages/backend/src/services/logger.ts +++ b/packages/backend/src/services/logger.ts @@ -2,8 +2,7 @@ import cluster from "node:cluster"; import chalk from "chalk"; import { default as convertColor } from "color-convert"; import { format as dateFormat } from "date-fns"; -import { envOption } from "@/env.js"; -import config from "@/config/index.js"; +import config, { envOption } from "@/config/index.js"; import * as SyslogPro from "syslog-pro"; From 80b80277e2d87588ef210867140eec3b9d4cba75 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Tue, 16 Apr 2024 01:50:42 +0900 Subject: [PATCH 067/110] fix (pug): random MOTD not showing --- packages/backend/src/server/web/views/base.pug | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index bdbe153fbe..738d3ffc01 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -72,8 +72,8 @@ html div#splash img#splashIcon(src= splashIcon || `/static-assets/splash.svg?${ timestamp }`) span#splashText - block randomMOTD - = randomMOTD + block randomMotd + = randomMotd div#splashSpinner <svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg"> <g transform="matrix(1,0,0,1,12,12)"> From 38192052c909c4583ca245f6102806ce787504bb Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Tue, 16 Apr 2024 05:34:43 +0900 Subject: [PATCH 068/110] meta: update issue/merge request templates --- .gitlab/issue_templates/bug.md | 32 ++++++++++++++-------- .gitlab/issue_templates/feature.md | 12 ++++---- .gitlab/merge_request_templates/default.md | 4 +-- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md index 3bffa21cde..a94fae0f0d 100644 --- a/.gitlab/issue_templates/bug.md +++ b/.gitlab/issue_templates/bug.md @@ -3,30 +3,38 @@ 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://firefish.dev/firefish/firefish/-/blob/develop/SECURITY.md) 🤝 By submitting this issue, you agree to follow our [Contribution Guidelines.](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) --> -**What happened?** _(Please give us a brief description of what happened.)_ +## What happened? <!-- Please give us a brief description of what happened. --> -**What did you expect to happen?** _(Please give us a brief description of what you expected to happen.)_ +## What did you expect to happen? <!-- Please give us a brief description of what you expected to happen. --> -**Version** _(What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.)_ +## Version <!-- What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information. --> -**Instance** _(What instance of firefish are you using?)_ +## What type of issue is this? <!-- If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side. --> -**What type of issue is this?** _(If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side.)_ +- [] server-side +- [] client-side +- [] not sure -**What browser are you using? (Client-side issues only)** +<details> -**What operating system are you using? (Client-side issues only)** +### Instance <!-- What instance of firefish are you using? --> -**How do you deploy Firefish on your server? (Server-side issues only)** +### What browser are you using? (client-side issues only) -**What operating system are you using? (Server-side issues only)** +### What operating system are you using? (client-side issues only) -**Relevant log output** _(Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks.)_ +### How do you deploy Firefish on your server? (server-side issues only) -**Contribution Guidelines** +### What operating system are you using? (Server-side issues only) + +### Relevant log output <!-- Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. --> + +</details> + +## Contribution Guidelines By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines - [ ] I have searched the issue tracker for similar issues, and this is not a duplicate. -**Are you willing to fix this bug?** (optional) +## Are you willing to fix this bug? (optional) - [ ] Yes. I will fix this bug and open a merge request if the change is agreed upon. diff --git a/.gitlab/issue_templates/feature.md b/.gitlab/issue_templates/feature.md index b4af4884b7..7b4917ecb3 100644 --- a/.gitlab/issue_templates/feature.md +++ b/.gitlab/issue_templates/feature.md @@ -3,18 +3,18 @@ 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://firefish.dev/firefish/firefish/-/blob/develop/SECURITY.md) 🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) --> -**What feature would you like implemented?** _(Please give us a brief description of what you'd like.)_ +## What feature would you like implemented? <!-- Please give us a brief description of what you'd like. --> -**Why should we add this feature?** _(Please give us a brief description of why your feature is important.)_ +## Why should we add this feature? <!-- Please give us a brief description of why your feature is important. --> -**Version** _(What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.)_ +## Version <!-- What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information. --> -**Instance** _(What instance of firefish are you using?)_ +## Instance <!-- What instance of firefish are you using? --> -**Contribution Guidelines** +## Contribution Guidelines By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines - [ ] I have searched the issue tracker for similar requests, and this is not a duplicate. -**Are you willing to implement this feature?** (optional) +## Are you willing to implement this feature? (optional) - [ ] Yes. I will implement this feature and open a merge request if the change is agreed upon. diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md index 2a1c926223..c2382481e3 100644 --- a/.gitlab/merge_request_templates/default.md +++ b/.gitlab/merge_request_templates/default.md @@ -1,8 +1,8 @@ <!-- Thanks for taking the time to make Firefish better! It's not required, but please consider using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) when making your commits. If you use VSCode, please use the [Conventional Commits extension](https://marketplace.visualstudio.com/items?itemName=vivaxy.vscode-conventional-commits). --> -**What does this PR do?** _(Please give us a brief description of what this PR does.)_ +## What does this PR do? <!-- Please give us a brief description of what this PR does. --> -**Contribution Guidelines** +## Contribution Guidelines By submitting this merge request, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) - [ ] This change is reviewed in an issue / This is a minor bug fix - [ ] I agree to follow this project's Contribution Guidelines From fd333250c9801fea344216778059ad8c3b3a5978 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Tue, 16 Apr 2024 08:56:05 +0900 Subject: [PATCH 069/110] chore (backend): set proxyRemoteFiles to true by default (close #9426) --- .config/example.yml | 2 +- packages/backend/src/config/load.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.config/example.yml b/.config/example.yml index 9082dfb868..fdfb0b0965 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -178,7 +178,7 @@ logLevel: [ # Media Proxy #mediaProxy: https://example.com/proxy -# Proxy remote files (default: false) +# Proxy remote files (default: true) #proxyRemoteFiles: true #allowedPrivateNetworks: [ diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts index 2b286b9439..682bf309d2 100644 --- a/packages/backend/src/config/load.ts +++ b/packages/backend/src/config/load.ts @@ -55,6 +55,7 @@ export default function load() { mixin.userAgent = `Firefish/${meta.version} (${config.url})`; mixin.clientEntry = clientManifest["src/init.ts"]; + if (config.proxyRemoteFiles == null) config.proxyRemoteFiles = true; if (!config.redis.prefix) config.redis.prefix = mixin.hostname; if (config.cacheServer && !config.cacheServer.prefix) config.cacheServer.prefix = mixin.hostname; From 77a2bcfc4b6ba4505242e5ffb23eeaeade32dbb2 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Tue, 16 Apr 2024 09:01:12 +0900 Subject: [PATCH 070/110] chore: remove unused items from example config file --- .config/example.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.config/example.yml b/.config/example.yml index fdfb0b0965..17149f6c3a 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -185,12 +185,6 @@ logLevel: [ # '127.0.0.1/32' #] -# TWA -#twa: -# nameSpace: android_app -# packageName: tld.domain.twa -# sha256CertFingerprints: ['AB:CD:EF'] - # Upload or download file size limits (bytes) #maxFileSize: 262144000 From 07384a4f0f34e61df5b4f12dfc02304c18ad8961 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Tue, 16 Apr 2024 09:14:44 +0900 Subject: [PATCH 071/110] feat (backend): increase CW character limit (close #10876) --- docs/downgrade.sql | 6 ++++++ packages/backend-rs/src/model/entity/note.rs | 1 + ...713225866247-convert-cw-varchar-to-text.ts | 21 +++++++++++++++++++ packages/backend/src/models/entities/note.ts | 5 ++--- .../src/server/api/endpoints/notes/create.ts | 2 +- 5 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts diff --git a/docs/downgrade.sql b/docs/downgrade.sql index 0b6f25e08c..77dea27573 100644 --- a/docs/downgrade.sql +++ b/docs/downgrade.sql @@ -1,6 +1,7 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'ConvertCwVarcharToText1713225866247', 'FixChatFileConstraint1712855579316', 'DropTimeZone1712425488543', 'ExpandNoteEdit1711936358554', @@ -22,6 +23,11 @@ DELETE FROM "migrations" WHERE name IN ( 'RemoveNativeUtilsMigration1705877093218' ); +--convert-cw-varchar-to-text +DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"; +ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512); +CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2); + -- fix-chat-file-constraint ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_535def119223ac05ad3fa9ef64b"; ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_535def119223ac05ad3fa9ef64b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION; diff --git a/packages/backend-rs/src/model/entity/note.rs b/packages/backend-rs/src/model/entity/note.rs index cb82f3d94a..5903216c1a 100644 --- a/packages/backend-rs/src/model/entity/note.rs +++ b/packages/backend-rs/src/model/entity/note.rs @@ -21,6 +21,7 @@ pub struct Model { #[sea_orm(column_type = "Text", nullable)] pub text: Option<String>, pub name: Option<String>, + #[sea_orm(column_type = "Text", nullable)] pub cw: Option<String>, #[sea_orm(column_name = "userId")] pub user_id: String, diff --git a/packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts b/packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts new file mode 100644 index 0000000000..93c87a98a8 --- /dev/null +++ b/packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts @@ -0,0 +1,21 @@ +import type { MigrationInterface, QueryRunner } from "typeorm"; + +export class ConvertCwVarcharToText1713225866247 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise<void> { + queryRunner.query(`DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"`); + queryRunner.query(`ALTER TABLE "note" ALTER COLUMN "cw" TYPE text`); + queryRunner.query( + `CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + queryRunner.query(`DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"`); + queryRunner.query( + `ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512)`, + ); + queryRunner.query( + `CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2)`, + ); + } +} diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index 738e43d442..94cd8c7b66 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -72,9 +72,8 @@ export class Note { }) public name: string | null; - @Index() // USING pgroonga pgroonga_varchar_full_text_search_ops_v2 - @Column("varchar", { - length: 512, + @Index() // USING pgroonga + @Column("text", { nullable: true, }) public cw: string | null; diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 270c33abd0..c2302f4c8d 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -114,7 +114,7 @@ export const paramDef = { enum: Object.keys(langmap), nullable: true, }, - cw: { type: "string", nullable: true, maxLength: 100 }, + cw: { type: "string", nullable: true, maxLength: MAX_NOTE_TEXT_LENGTH }, localOnly: { type: "boolean", default: false }, noExtractMentions: { type: "boolean", default: false }, noExtractHashtags: { type: "boolean", default: false }, From 7af2cca2d4e507e8e50ada87ea1488d407df9c17 Mon Sep 17 00:00:00 2001 From: Gary O'Regan Kelly <gmoregan@icloud.com> Date: Mon, 15 Apr 2024 02:27:04 +0000 Subject: [PATCH 072/110] locale: update translations (French) Currently translated at 100.0% (1920 of 1920 strings) Translation: Firefish/locales Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/ --- locales/fr-FR.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index da56856077..14c46b1740 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -2324,3 +2324,4 @@ markLocalFilesNsfwByDefaultDescription: Indépendamment de ce réglage, les util peuvent supprimer le drapeau « sensible » (NSFW) eux-mêmes. Les fichiers existants ne sont pas affectés. noteEditHistory: Historique des publications +media: Multimédia From bf3c0717b90a69071486852e8ea0ded054e9a6d8 Mon Sep 17 00:00:00 2001 From: yumeko <yumeko@mainichi.social> Date: Tue, 16 Apr 2024 15:29:18 +0000 Subject: [PATCH 073/110] Fix check for whether stats are disabled in meta in server machine stats job --- packages/backend/src/daemons/server-stats.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts index 92265ecba7..df1f9b3032 100644 --- a/packages/backend/src/daemons/server-stats.ts +++ b/packages/backend/src/daemons/server-stats.ts @@ -13,16 +13,15 @@ const round = (num: number) => Math.round(num * 10) / 10; /** * Report server stats regularly */ -export default function () { +export default async function () { const log = [] as any[]; ev.on("requestServerStatsLog", (x) => { ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50)); }); - fetchMeta(true).then((meta) => { - if (!meta.enableServerMachineStats) return; - }); + const meta = await fetchMeta(true); + if (!meta.enableServerMachineStats) return; async function tick() { const cpu = await cpuUsage(); From 17fb05430ede5825d7f03c9b0d74c7b57318801d Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Wed, 17 Apr 2024 17:46:23 +0900 Subject: [PATCH 074/110] fix (backend, Mastodon API): add 'meta.original' field to media attachments addresses https://github.com/whitescent/Mastify/pull/102 --- .../src/server/api/mastodon/converters.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index b392403578..b2259e6ed5 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 "megalodon"; +import type { Entity } from "megalodon"; import { toMastodonId } from "backend-rs"; function simpleConvert(data: any) { @@ -15,7 +15,19 @@ export function convertAnnouncement(announcement: Entity.Announcement) { return simpleConvert(announcement); } export function convertAttachment(attachment: Entity.Attachment) { - return simpleConvert(attachment); + const converted = simpleConvert(attachment); + // ref: https://github.com/whitescent/Mastify/pull/102 + if (converted.meta == null) return converted; + const result = { + ...converted, + meta: { + ...converted.meta, + original: { + ...converted.meta, + }, + }, + }; + return result; } export function convertFilter(filter: Entity.Filter) { return simpleConvert(filter); From 84890661307ee6177639bf4a7f2ab961059b7c75 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Wed, 17 Apr 2024 19:06:29 +0900 Subject: [PATCH 075/110] fix (client): list layout on mobile --- .../client/src/pages/user-list-timeline.vue | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/client/src/pages/user-list-timeline.vue b/packages/client/src/pages/user-list-timeline.vue index 69f17c1047..92a25ddd8d 100644 --- a/packages/client/src/pages/user-list-timeline.vue +++ b/packages/client/src/pages/user-list-timeline.vue @@ -3,7 +3,7 @@ <template #header ><MkPageHeader :actions="headerActions" :tabs="headerTabs" /></template> - <div ref="rootEl" class="eqqrhokj"> + <MkSpacer> <div class="tl _block"> <XTimeline ref="tlEl" @@ -14,12 +14,15 @@ :sound="true" /> </div> - </div> + </MkSpacer> </MkStickyContainer> </template> <script lang="ts" setup> import { computed, ref, watch } from "vue"; +import type { entities } from "firefish-js"; +// TODO: disable this rule properly +// biome-ignore lint/style/useImportType: used in <template> import XTimeline from "@/components/MkTimeline.vue"; import * as os from "@/os"; import { useRouter } from "@/router"; @@ -33,9 +36,8 @@ const props = defineProps<{ listId: string; }>(); -const list = ref(null); +const list = ref<entities.UserList>(); const tlEl = ref<InstanceType<typeof XTimeline>>(); -const rootEl = ref<HTMLElement>(); watch( () => props.listId, @@ -92,13 +94,8 @@ definePageMetadata( </script> <style lang="scss" scoped> -.eqqrhokj { - padding: var(--margin); - max-width: 800px; - margin: 0 auto; - > .tl { - background: none; - border-radius: var(--radius); - } +.tl { + background: none; + border-radius: var(--radius); } </style> From ec7578e78e652c2f2a152c4945d678f067ab48c2 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Wed, 17 Apr 2024 19:49:02 +0900 Subject: [PATCH 076/110] docs: specify max-old-space-size in example config files --- docker-compose.example.yml | 1 + docs/install.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index fc6c0268a2..9cd6d1cdce 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -17,6 +17,7 @@ services: # - web environment: NODE_ENV: production + NODE_OPTIONS: --max-old-space-size=3072 volumes: - ./custom:/firefish/custom:ro - ./files:/firefish/files diff --git a/docs/install.md b/docs/install.md index 061000fa32..324923c6a7 100644 --- a/docs/install.md +++ b/docs/install.md @@ -154,7 +154,7 @@ sudo apt install ffmpeg 1. Build ```sh pnpm install --frozen-lockfile - NODE_ENV=production pnpm run build + NODE_ENV=production NODE_OPTIONS='--max-old-space-size=3072' pnpm run build ``` 1. Execute database migrations ```sh @@ -242,6 +242,7 @@ In this instruction, we use [Caddy](https://caddyserver.com/) to make the Firefi WorkingDirectory=/home/firefish/firefish Environment="NODE_ENV=production" Environment="npm_config_cache=/tmp" + Environment="NODE_OPTIONS=--max-old-space-size=3072" # uncomment the following line if you use jemalloc (note that the path varies on different environments) # Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2" StandardOutput=journal From 22f4278ab5ee7a66cdaf3bbc234301b165a8c166 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Wed, 17 Apr 2024 23:14:43 +0900 Subject: [PATCH 077/110] meta: update issue/merge request templates --- .gitlab/issue_templates/bug.md | 15 ++++++++++++--- .gitlab/issue_templates/feature.md | 4 ++++ .gitlab/merge_request_templates/default.md | 1 + 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md index a94fae0f0d..f96bdec01e 100644 --- a/.gitlab/issue_templates/bug.md +++ b/.gitlab/issue_templates/bug.md @@ -5,30 +5,39 @@ ## What happened? <!-- Please give us a brief description of what happened. --> + ## What did you expect to happen? <!-- Please give us a brief description of what you expected to happen. --> + ## Version <!-- What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information. --> + ## What type of issue is this? <!-- If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side. --> -- [] server-side -- [] client-side -- [] not sure +- [ ] server-side +- [ ] client-side +- [ ] not sure <details> ### Instance <!-- What instance of firefish are you using? --> + ### What browser are you using? (client-side issues only) + ### What operating system are you using? (client-side issues only) + ### How do you deploy Firefish on your server? (server-side issues only) + ### What operating system are you using? (Server-side issues only) + ### Relevant log output <!-- Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. --> + </details> ## Contribution Guidelines diff --git a/.gitlab/issue_templates/feature.md b/.gitlab/issue_templates/feature.md index 7b4917ecb3..4c9ee56226 100644 --- a/.gitlab/issue_templates/feature.md +++ b/.gitlab/issue_templates/feature.md @@ -5,12 +5,16 @@ ## What feature would you like implemented? <!-- Please give us a brief description of what you'd like. --> + ## Why should we add this feature? <!-- Please give us a brief description of why your feature is important. --> + ## Version <!-- What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information. --> + ## Instance <!-- What instance of firefish are you using? --> + ## Contribution Guidelines By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md index c2382481e3..d13a146da0 100644 --- a/.gitlab/merge_request_templates/default.md +++ b/.gitlab/merge_request_templates/default.md @@ -2,6 +2,7 @@ ## What does this PR do? <!-- Please give us a brief description of what this PR does. --> + ## Contribution Guidelines By submitting this merge request, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) - [ ] This change is reviewed in an issue / This is a minor bug fix From a411f4e4d9ee61ca68ff286d6c9e543d5f719fc7 Mon Sep 17 00:00:00 2001 From: yumeko <yumeko@mainichi.social> Date: Wed, 17 Apr 2024 18:19:17 +0000 Subject: [PATCH 078/110] Fix internal error in api/pinned-users if one or more name fails to resolve --- packages/backend/src/server/api/endpoints/pinned-users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 325b54f350..5685b698eb 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -45,7 +45,7 @@ export default define(meta, paramDef, async (ps, me) => { ); return await Users.packMany( - users.filter((x) => x !== undefined) as User[], + users.filter((x) => (x != null)) as User[], me, { detail: true }, ); From 8337863ed3da565224a966d2489225f485015dfb Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Thu, 18 Apr 2024 05:01:49 +0900 Subject: [PATCH 079/110] chore: format --- packages/backend/src/server/api/endpoints/pinned-users.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 5685b698eb..65241becae 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -44,9 +44,7 @@ export default define(meta, paramDef, async (ps, me) => { ), ); - return await Users.packMany( - users.filter((x) => (x != null)) as User[], - me, - { detail: true }, - ); + return await Users.packMany(users.filter((x) => x != null) as User[], me, { + detail: true, + }); }); From 30969ad81799b7688ae3565066a57ad4d2dee83d Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Thu, 18 Apr 2024 05:02:00 +0900 Subject: [PATCH 080/110] refactor (backend): port get-note-summary to backend-rs I removed trim() as it wasn't strictly neccessary --- packages/backend-rs/index.d.ts | 13 ++- packages/backend-rs/index.js | 3 +- .../backend-rs/src/misc/check_word_mute.rs | 3 +- .../backend-rs/src/misc/get_note_summary.rs | 90 +++++++++++++++++++ packages/backend-rs/src/misc/mod.rs | 1 + packages/backend/src/misc/get-note-summary.ts | 53 ----------- packages/backend/src/models/schema/note.ts | 9 +- packages/backend/src/server/web/index.ts | 11 ++- .../backend/src/services/push-notification.ts | 11 ++- 9 files changed, 123 insertions(+), 71 deletions(-) create mode 100644 packages/backend-rs/src/misc/get_note_summary.rs delete mode 100644 packages/backend/src/misc/get-note-summary.ts diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index ae050e01d8..5ee69969c6 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -128,7 +128,8 @@ export interface Acct { } export function stringToAcct(acct: string): Acct export function acctToString(acct: Acct): string -export interface NoteLike { +/** TODO: handle name collisions better */ +export interface NoteLikeForCheckWordMute { fileIds: Array<string> userId: string | null text: string | null @@ -136,7 +137,7 @@ export interface NoteLike { renoteId: string | null replyId: string | null } -export function checkWordMute(note: NoteLike, mutedWordLists: Array<Array<string>>, mutedPatterns: Array<string>): Promise<boolean> +export function checkWordMute(note: NoteLikeForCheckWordMute, mutedWordLists: Array<Array<string>>, mutedPatterns: Array<string>): Promise<boolean> export function getFullApAccount(username: string, host?: string | undefined | null): string export function isSelfHost(host?: string | undefined | null): boolean export function isSameOrigin(uri: string): boolean @@ -147,6 +148,14 @@ export function sqlLikeEscape(src: string): string export function safeForSql(src: string): boolean /** Convert milliseconds to a human readable string */ export function formatMilliseconds(milliseconds: number): string +/** TODO: handle name collisions better */ +export interface NoteLikeForGetNoteSummary { + fileIds: Array<string> + text: string | null + cw: string | null + hasPoll: boolean +} +export function getNoteSummary(note: NoteLikeForGetNoteSummary): string export function toMastodonId(firefishId: string): string | null export function fromMastodonId(mastodonId: string): string | null export function fetchMeta(useCache: boolean): Promise<Meta> diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 6acd3ca68d..1ea7bb5bed 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding module.exports.readEnvironmentConfig = readEnvironmentConfig module.exports.readServerConfig = readServerConfig @@ -326,6 +326,7 @@ module.exports.isUnicodeEmoji = isUnicodeEmoji module.exports.sqlLikeEscape = sqlLikeEscape module.exports.safeForSql = safeForSql module.exports.formatMilliseconds = formatMilliseconds +module.exports.getNoteSummary = getNoteSummary module.exports.toMastodonId = toMastodonId module.exports.fromMastodonId = fromMastodonId module.exports.fetchMeta = fetchMeta diff --git a/packages/backend-rs/src/misc/check_word_mute.rs b/packages/backend-rs/src/misc/check_word_mute.rs index 801175c2af..18b550c29b 100644 --- a/packages/backend-rs/src/misc/check_word_mute.rs +++ b/packages/backend-rs/src/misc/check_word_mute.rs @@ -4,7 +4,8 @@ use once_cell::sync::Lazy; use regex::Regex; use sea_orm::{prelude::*, QuerySelect}; -#[crate::export(object)] +/// TODO: handle name collisions better +#[crate::export(object, js_name = "NoteLikeForCheckWordMute")] pub struct NoteLike { pub file_ids: Vec<String>, pub user_id: Option<String>, diff --git a/packages/backend-rs/src/misc/get_note_summary.rs b/packages/backend-rs/src/misc/get_note_summary.rs new file mode 100644 index 0000000000..3b759b04f5 --- /dev/null +++ b/packages/backend-rs/src/misc/get_note_summary.rs @@ -0,0 +1,90 @@ +/// TODO: handle name collisions better +#[crate::export(object, js_name = "NoteLikeForGetNoteSummary")] +pub struct NoteLike { + pub file_ids: Vec<String>, + pub text: Option<String>, + pub cw: Option<String>, + pub has_poll: bool, +} + +#[crate::export] +pub fn get_note_summary(note: NoteLike) -> String { + let mut buf: Vec<String> = vec![]; + + if let Some(cw) = note.cw { + buf.push(cw) + } else if let Some(text) = note.text { + buf.push(text) + } + + match note.file_ids.len() { + 0 => (), + 1 => buf.push("📎".to_string()), + n => buf.push(format!("📎 ({})", n)), + }; + + if note.has_poll { + buf.push("📊".to_string()) + } + + buf.join(" ") +} + +#[cfg(test)] +mod unit_test { + use super::{get_note_summary, NoteLike}; + use pretty_assertions::assert_eq; + + #[test] + fn test_note_summary() { + let note = NoteLike { + file_ids: vec![], + text: Some("Hello world!".to_string()), + cw: None, + has_poll: false, + }; + assert_eq!(get_note_summary(note), "Hello world!"); + + let note_with_cw = NoteLike { + file_ids: vec![], + text: Some("Hello world!".to_string()), + cw: Some("Content warning".to_string()), + has_poll: false, + }; + assert_eq!(get_note_summary(note_with_cw), "Content warning"); + + let note_with_file_and_cw = NoteLike { + file_ids: vec!["9s7fmcqogiq4igin".to_string()], + text: None, + cw: Some("Selfie, no ec".to_string()), + has_poll: false, + }; + assert_eq!(get_note_summary(note_with_file_and_cw), "Selfie, no ec 📎"); + + let note_with_files_only = NoteLike { + file_ids: vec![ + "9s7fmcqogiq4igin".to_string(), + "9s7qrld5u14cey98".to_string(), + "9s7gebs5zgts4kca".to_string(), + "9s5z3e4vefqd29ee".to_string(), + ], + text: None, + cw: None, + has_poll: false, + }; + assert_eq!(get_note_summary(note_with_files_only), "📎 (4)"); + + let note_all = NoteLike { + file_ids: vec![ + "9s7fmcqogiq4igin".to_string(), + "9s7qrld5u14cey98".to_string(), + "9s7gebs5zgts4kca".to_string(), + "9s5z3e4vefqd29ee".to_string(), + ], + text: Some("Hello world!".to_string()), + cw: Some("Content warning".to_string()), + has_poll: true, + }; + assert_eq!(get_note_summary(note_all), "Content warning 📎 (4) 📊"); + } +} diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index 45fd31cdcd..a9d7074dbf 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -4,6 +4,7 @@ pub mod convert_host; pub mod emoji; pub mod escape_sql; pub mod format_milliseconds; +pub mod get_note_summary; pub mod mastodon_id; pub mod meta; pub mod nyaify; diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts deleted file mode 100644 index 0a662e434e..0000000000 --- a/packages/backend/src/misc/get-note-summary.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Packed } from "./schema.js"; - -/** - * 投稿を表す文字列を取得します。 - * @param {*} note (packされた)投稿 - */ -export const getNoteSummary = (note: Packed<"Note">): string => { - if (note.deletedAt) { - return "❌"; - } - - let summary = ""; - - // 本文 - if (note.cw != null) { - summary += note.cw; - } else { - summary += note.text ? note.text : ""; - } - - // ファイルが添付されているとき - if ((note.files || []).length !== 0) { - const len = note.files?.length; - summary += ` 📎${len !== 1 ? ` (${len})` : ""}`; - } - - // 投票が添付されているとき - if (note.poll) { - summary += " 📊"; - } - - /* - // 返信のとき - if (note.replyId) { - if (note.reply) { - summary += `\n\nRE: ${getNoteSummary(note.reply)}`; - } else { - summary += '\n\nRE: ...'; - } - } - - // Renoteのとき - if (note.renoteId) { - if (note.renote) { - summary += `\n\nRN: ${getNoteSummary(note.renote)}`; - } else { - summary += '\n\nRN: ...'; - } - } - */ - - return summary.trim(); -}; diff --git a/packages/backend/src/models/schema/note.ts b/packages/backend/src/models/schema/note.ts index 7dcdbc9b03..fff872b69f 100644 --- a/packages/backend/src/models/schema/note.ts +++ b/packages/backend/src/models/schema/note.ts @@ -28,7 +28,7 @@ export const packedNoteSchema = { }, cw: { type: "string", - optional: true, + optional: false, nullable: true, }, userId: { @@ -98,7 +98,7 @@ export const packedNoteSchema = { }, fileIds: { type: "array", - optional: true, + optional: false, nullable: false, items: { type: "string", @@ -128,6 +128,11 @@ export const packedNoteSchema = { nullable: false, }, }, + hasPoll: { + type: "boolean", + optional: false, + nullable: false, + }, poll: { type: "object", optional: true, diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 6473073370..939fcfab14 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -27,8 +27,7 @@ import { Emojis, GalleryPosts, } from "@/models/index.js"; -import { stringToAcct } from "backend-rs"; -import { getNoteSummary } from "@/misc/get-note-summary.js"; +import { getNoteSummary, stringToAcct } from "backend-rs"; import { queues } from "@/queue/queues.js"; import { genOpenapiSpec } from "../api/openapi/gen-spec.js"; import { urlPreviewHandler } from "./url-preview.js"; @@ -517,8 +516,8 @@ router.get("/notes/:note", async (ctx, next) => { }); try { - if (note) { - const _note = await Notes.pack(note); + if (note != null) { + const packedNote = await Notes.pack(note); const profile = await UserProfiles.findOneByOrFail({ userId: note.userId, @@ -526,13 +525,13 @@ router.get("/notes/:note", async (ctx, next) => { const meta = await fetchMeta(true); await ctx.render("note", { ...metaToPugArgs(meta), - note: _note, + note: packedNote, profile, avatarUrl: await Users.getAvatarUrl( await Users.findOneByOrFail({ id: note.userId }), ), // TODO: Let locale changeable by instance setting - summary: getNoteSummary(_note), + summary: getNoteSummary(note), }); ctx.set("Cache-Control", "public, max-age=15"); diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts index 1a772ff9c5..3f1f2cfb1a 100644 --- a/packages/backend/src/services/push-notification.ts +++ b/packages/backend/src/services/push-notification.ts @@ -1,9 +1,8 @@ import push from "web-push"; import config from "@/config/index.js"; import { SwSubscriptions } from "@/models/index.js"; -import { fetchMeta } from "backend-rs"; +import { fetchMeta, getNoteSummary } from "backend-rs"; import type { Packed } from "@/misc/schema.js"; -import { getNoteSummary } from "@/misc/get-note-summary.js"; // Defined also packages/sw/types.ts#L14-L21 type pushNotificationsTypes = { @@ -17,15 +16,15 @@ type pushNotificationsTypes = { // プッシュメッセージサーバーには文字数制限があるため、内容を削減します function truncateNotification(notification: Packed<"Notification">): any { - if (notification.note) { + if (notification.note != null) { return { ...notification, note: { ...notification.note, - // textをgetNoteSummaryしたものに置き換える + // replace the text with summary text: getNoteSummary( - notification.type === "renote" - ? (notification.note.renote as Packed<"Note">) + notification.type === "renote" && notification.note.renote != null + ? notification.note.renote : notification.note, ), From c19c439ac11692efd15e5fade5bed13ae52f9886 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Thu, 18 Apr 2024 05:08:14 +0900 Subject: [PATCH 081/110] fix (backend): add hasPoll to packed note --- packages/backend/src/models/repositories/note.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index 1bf4ef657f..c877048709 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -232,6 +232,7 @@ export const NoteRepository = db.getRepository(Note).extend({ uri: note.uri || undefined, url: note.url || undefined, updatedAt: note.updatedAt?.toISOString() || undefined, + hasPoll: note.hasPoll, poll: note.hasPoll ? populatePoll(note, meId) : undefined, ...(meId ? { From ff08d044b574aaa1ba36922bf97d64f461067641 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Thu, 18 Apr 2024 05:22:54 +0900 Subject: [PATCH 082/110] chore: format --- packages/client/src/components/MkMenu.vue | 2 +- packages/client/src/os.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue index 1391024264..a90e42fc29 100644 --- a/packages/client/src/components/MkMenu.vue +++ b/packages/client/src/components/MkMenu.vue @@ -244,7 +244,7 @@ const itemsEl = ref<HTMLDivElement>(); /** * Strictly speaking, this type conversion is wrong - * because `ref` will deeply unpack the `ref` in `MenuSwitch`. + * because `ref` will deeply unpack the `ref` in `MenuSwitch`. * But it performs correctly, so who cares? */ const items2 = ref([]) as Ref<InnerMenuItem[]>; diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 0bdacd3adb..8fbe8b8041 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -841,7 +841,9 @@ export async function openEmojiPicker( activeTextarea = initialTextarea; - const textareas = document.querySelectorAll<HTMLTextAreaElement | HTMLInputElement>("textarea, input"); + const textareas = document.querySelectorAll< + HTMLTextAreaElement | HTMLInputElement + >("textarea, input"); for (const textarea of Array.from(textareas)) { textarea.addEventListener("focus", () => { activeTextarea = textarea; @@ -853,7 +855,9 @@ export async function openEmojiPicker( for (const node of Array.from(record.addedNodes).filter( (node) => node instanceof HTMLElement, ) as HTMLElement[]) { - const textareas = node.querySelectorAll<HTMLTextAreaElement | HTMLInputElement>("textarea, input"); + const textareas = node.querySelectorAll< + HTMLTextAreaElement | HTMLInputElement + >("textarea, input"); for (const textarea of Array.from(textareas).filter( (textarea) => textarea.dataset.preventEmojiInsert == null, )) { From 78092cd4bedf339176ba367580bd8f5c7dc9c960 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Thu, 18 Apr 2024 05:32:42 +0900 Subject: [PATCH 083/110] dev (client): update eslint rules --- packages/client/.eslintrc.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/.eslintrc.json b/packages/client/.eslintrc.json index b0e97b2fa6..37d80f6588 100644 --- a/packages/client/.eslintrc.json +++ b/packages/client/.eslintrc.json @@ -4,10 +4,10 @@ "ignorePatterns": ["**/*.json5"], "rules": { "file-progress/activate": 1, - "prettier/prettier": 0, - "one-var": ["error", "never"], + "prettier/prettier": "off", + "one-var": ["warn", "never"], "@typescript-eslint/no-unused-vars": [ - "error", + "warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", From 9d876798004f3dcbe812cab2c08cfc221625f910 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Thu, 18 Apr 2024 05:34:23 +0900 Subject: [PATCH 084/110] chore: lint --- packages/client/src/components/MkAbuseReport.vue | 2 +- packages/client/src/components/MkAnnouncement.vue | 2 +- packages/client/src/components/MkAvatars.vue | 2 +- packages/client/src/components/MkChannelFollowButton.vue | 2 +- packages/client/src/components/MkChannelPreview.vue | 2 +- packages/client/src/components/MkContainer.vue | 2 +- packages/client/src/components/MkGalleryPostPreview.vue | 2 +- packages/client/src/components/MkInstanceStats.vue | 2 +- packages/client/src/components/MkInstanceTicker.vue | 2 +- packages/client/src/components/MkMediaCaption.vue | 2 +- packages/client/src/components/MkNotes.vue | 2 +- packages/client/src/components/MkNotification.vue | 4 ++-- packages/client/src/components/MkNotificationToast.vue | 2 +- packages/client/src/components/MkPagePreview.vue | 2 +- packages/client/src/components/MkReactionTooltip.vue | 2 +- .../client/src/components/MkReactionsViewer.details.vue | 4 ++-- packages/client/src/components/MkSignin.vue | 2 +- packages/client/src/components/MkUserList.vue | 2 +- packages/client/src/components/MkUsersTooltip.vue | 4 ++-- packages/client/src/components/MkWaitingDialog.vue | 3 ++- packages/client/src/os.ts | 6 +++--- packages/client/src/pages/admin/abuses.vue | 2 +- packages/client/src/pages/channel-editor.vue | 2 +- packages/client/src/pages/channel.vue | 2 +- packages/client/src/pages/channels.vue | 2 +- packages/client/src/pages/gallery/post.vue | 2 +- packages/client/src/pages/my-antennas/index.vue | 3 ++- packages/client/src/pages/note-history.vue | 2 +- packages/client/src/scripts/use-chart-tooltip.ts | 6 +++--- packages/client/src/store.ts | 2 +- packages/client/src/types/form.ts | 4 ++-- packages/client/src/types/note.ts | 4 ++-- packages/client/src/types/post-form.ts | 4 ++-- 33 files changed, 45 insertions(+), 43 deletions(-) diff --git a/packages/client/src/components/MkAbuseReport.vue b/packages/client/src/components/MkAbuseReport.vue index 26b91fae2e..b190652052 100644 --- a/packages/client/src/components/MkAbuseReport.vue +++ b/packages/client/src/components/MkAbuseReport.vue @@ -67,12 +67,12 @@ <script lang="ts" setup> import { ref } from "vue"; +import type { entities } from "firefish-js"; import MkButton from "@/components/MkButton.vue"; import MkSwitch from "@/components/form/switch.vue"; import MkKeyValue from "@/components/MkKeyValue.vue"; import * as os from "@/os"; import { i18n } from "@/i18n"; -import type { entities } from "firefish-js"; const props = defineProps<{ report: entities.AbuseUserReport; diff --git a/packages/client/src/components/MkAnnouncement.vue b/packages/client/src/components/MkAnnouncement.vue index 0aaa519cb0..3af8e0163f 100644 --- a/packages/client/src/components/MkAnnouncement.vue +++ b/packages/client/src/components/MkAnnouncement.vue @@ -30,12 +30,12 @@ <script lang="ts" setup> import { shallowRef } from "vue"; +import type { entities } from "firefish-js"; import MkModal from "@/components/MkModal.vue"; import MkSparkle from "@/components/MkSparkle.vue"; import MkButton from "@/components/MkButton.vue"; import { i18n } from "@/i18n"; import * as os from "@/os"; -import type { entities } from "firefish-js"; const props = defineProps<{ announcement: entities.Announcement; diff --git a/packages/client/src/components/MkAvatars.vue b/packages/client/src/components/MkAvatars.vue index 2b2029a8b5..c1edf1e391 100644 --- a/packages/client/src/components/MkAvatars.vue +++ b/packages/client/src/components/MkAvatars.vue @@ -8,8 +8,8 @@ <script lang="ts" setup> import { onMounted, ref } from "vue"; -import * as os from "@/os"; import type { entities } from "firefish-js"; +import * as os from "@/os"; const props = defineProps<{ userIds: string[]; diff --git a/packages/client/src/components/MkChannelFollowButton.vue b/packages/client/src/components/MkChannelFollowButton.vue index 98e239195c..3ff907b25b 100644 --- a/packages/client/src/components/MkChannelFollowButton.vue +++ b/packages/client/src/components/MkChannelFollowButton.vue @@ -24,10 +24,10 @@ <script lang="ts" setup> import { ref } from "vue"; +import type { entities } from "firefish-js"; import * as os from "@/os"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; -import type { entities } from "firefish-js"; const props = withDefaults( defineProps<{ diff --git a/packages/client/src/components/MkChannelPreview.vue b/packages/client/src/components/MkChannelPreview.vue index 8b2e12dc8b..b7462f5504 100644 --- a/packages/client/src/components/MkChannelPreview.vue +++ b/packages/client/src/components/MkChannelPreview.vue @@ -52,9 +52,9 @@ <script lang="ts" setup> import { computed } from "vue"; +import type { entities } from "firefish-js"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; -import type { entities } from "firefish-js"; const props = defineProps<{ channel: entities.Channel; diff --git a/packages/client/src/components/MkContainer.vue b/packages/client/src/components/MkContainer.vue index e933b02dbf..6f7f19dbf0 100644 --- a/packages/client/src/components/MkContainer.vue +++ b/packages/client/src/components/MkContainer.vue @@ -1,5 +1,6 @@ <template> <div + ref="el" v-size="{ max: [380] }" class="ukygtjoj _panel" :class="{ @@ -9,7 +10,6 @@ scrollable, closed: !showBody, }" - ref="el" > <header v-if="showHeader" ref="header"> <div class="title"><slot name="header"></slot></div> diff --git a/packages/client/src/components/MkGalleryPostPreview.vue b/packages/client/src/components/MkGalleryPostPreview.vue index cbef1c3d75..f4da5d20e8 100644 --- a/packages/client/src/components/MkGalleryPostPreview.vue +++ b/packages/client/src/components/MkGalleryPostPreview.vue @@ -33,9 +33,9 @@ </template> <script lang="ts" setup> +import type { entities } from "firefish-js"; import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue"; import { i18n } from "@/i18n"; -import type { entities } from "firefish-js"; defineProps<{ post: entities.GalleryPost; diff --git a/packages/client/src/components/MkInstanceStats.vue b/packages/client/src/components/MkInstanceStats.vue index 72f987ec84..87b1187e2d 100644 --- a/packages/client/src/components/MkInstanceStats.vue +++ b/packages/client/src/components/MkInstanceStats.vue @@ -44,6 +44,7 @@ <script lang="ts" setup> import { onMounted, ref, shallowRef } from "vue"; import { Chart } from "chart.js"; +import type { entities } from "firefish-js"; import MkSelect from "@/components/form/select.vue"; import MkChart from "@/components/MkChart.vue"; import { useChartTooltip } from "@/scripts/use-chart-tooltip"; @@ -52,7 +53,6 @@ import { i18n } from "@/i18n"; import MkActiveUsersHeatmap from "@/components/MkActiveUsersHeatmap.vue"; import MkFolder from "@/components/MkFolder.vue"; import { initChart } from "@/scripts/init-chart"; -import type { entities } from "firefish-js"; initChart(); diff --git a/packages/client/src/components/MkInstanceTicker.vue b/packages/client/src/components/MkInstanceTicker.vue index bbf999c183..f27cce02a3 100644 --- a/packages/client/src/components/MkInstanceTicker.vue +++ b/packages/client/src/components/MkInstanceTicker.vue @@ -17,10 +17,10 @@ <script lang="ts" setup> import { ref } from "vue"; +import type { entities } from "firefish-js"; import { instanceName, version } from "@/config"; import { instance as Instance } from "@/instance"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; -import type { entities } from "firefish-js"; const props = defineProps<{ instance?: entities.InstanceLite; diff --git a/packages/client/src/components/MkMediaCaption.vue b/packages/client/src/components/MkMediaCaption.vue index 32ecceda21..b09024adb7 100644 --- a/packages/client/src/components/MkMediaCaption.vue +++ b/packages/client/src/components/MkMediaCaption.vue @@ -69,6 +69,7 @@ import { computed, onBeforeUnmount, onMounted, ref } from "vue"; import insertTextAtCursor from "insert-text-at-cursor"; import { length } from "stringz"; +import type { entities } from "firefish-js"; import * as os from "@/os"; import MkModal from "@/components/MkModal.vue"; import MkButton from "@/components/MkButton.vue"; @@ -76,7 +77,6 @@ import bytes from "@/filters/bytes"; import number from "@/filters/number"; import { i18n } from "@/i18n"; import { instance } from "@/instance"; -import type { entities } from "firefish-js"; const props = withDefaults( defineProps<{ diff --git a/packages/client/src/components/MkNotes.vue b/packages/client/src/components/MkNotes.vue index d347962768..09db9dcaf0 100644 --- a/packages/client/src/components/MkNotes.vue +++ b/packages/client/src/components/MkNotes.vue @@ -40,12 +40,12 @@ <script lang="ts" setup> import { ref } from "vue"; +import type { entities } from "firefish-js"; import type { MkPaginationType, PagingKeyOf, PagingOf, } from "@/components/MkPagination.vue"; -import type { entities } from "firefish-js"; import XNote from "@/components/MkNote.vue"; import XList from "@/components/MkDateSeparatedList.vue"; import MkPagination from "@/components/MkPagination.vue"; diff --git a/packages/client/src/components/MkNotification.vue b/packages/client/src/components/MkNotification.vue index 6dd1a4b721..e3f8259def 100644 --- a/packages/client/src/components/MkNotification.vue +++ b/packages/client/src/components/MkNotification.vue @@ -272,6 +272,8 @@ <script lang="ts" setup> import { onMounted, onUnmounted, ref, toRef, watch } from "vue"; import type { entities } from "firefish-js"; +import type { Connection } from "firefish-js/src/streaming"; +import type { Channels } from "firefish-js/src/streaming.types"; import XReactionIcon from "@/components/MkReactionIcon.vue"; import MkFollowButton from "@/components/MkFollowButton.vue"; import XReactionTooltip from "@/components/MkReactionTooltip.vue"; @@ -285,8 +287,6 @@ import { useTooltip } from "@/scripts/use-tooltip"; import { defaultStore } from "@/store"; import { instance } from "@/instance"; import icon from "@/scripts/icon"; -import type { Connection } from "firefish-js/src/streaming"; -import type { Channels } from "firefish-js/src/streaming.types"; const props = withDefaults( defineProps<{ diff --git a/packages/client/src/components/MkNotificationToast.vue b/packages/client/src/components/MkNotificationToast.vue index a18a7e3d48..32b0727927 100644 --- a/packages/client/src/components/MkNotificationToast.vue +++ b/packages/client/src/components/MkNotificationToast.vue @@ -16,10 +16,10 @@ <script lang="ts" setup> import { onMounted, ref } from "vue"; +import type { entities } from "firefish-js"; import XNotification from "@/components/MkNotification.vue"; import * as os from "@/os"; import { defaultStore } from "@/store"; -import type { entities } from "firefish-js"; defineProps<{ notification: entities.Notification; diff --git a/packages/client/src/components/MkPagePreview.vue b/packages/client/src/components/MkPagePreview.vue index 3a6a45745d..8377770aa0 100644 --- a/packages/client/src/components/MkPagePreview.vue +++ b/packages/client/src/components/MkPagePreview.vue @@ -34,9 +34,9 @@ </template> <script lang="ts" setup> +import type { entities } from "firefish-js"; import { userName } from "@/filters/user"; import { ui } from "@/config"; -import type { entities } from "firefish-js"; defineProps<{ page: entities.Page; diff --git a/packages/client/src/components/MkReactionTooltip.vue b/packages/client/src/components/MkReactionTooltip.vue index 1286fc4c73..68a1d6b625 100644 --- a/packages/client/src/components/MkReactionTooltip.vue +++ b/packages/client/src/components/MkReactionTooltip.vue @@ -20,9 +20,9 @@ <script lang="ts" setup> import type { Ref } from "vue"; +import type { entities } from "firefish-js"; import MkTooltip from "./MkTooltip.vue"; import XReactionIcon from "@/components/MkReactionIcon.vue"; -import type { entities } from "firefish-js"; defineProps<{ showing: Ref<boolean>; diff --git a/packages/client/src/components/MkReactionsViewer.details.vue b/packages/client/src/components/MkReactionsViewer.details.vue index 14c2828d45..7c5f118f98 100644 --- a/packages/client/src/components/MkReactionsViewer.details.vue +++ b/packages/client/src/components/MkReactionsViewer.details.vue @@ -3,8 +3,8 @@ ref="tooltip" :target-element="targetElement" :max-width="340" - @closed="emit('closed')" :showing="showing" + @closed="emit('closed')" > <div class="bqxuuuey"> <div class="reaction"> @@ -31,9 +31,9 @@ <script lang="ts" setup> import type { Ref } from "vue"; +import type { entities } from "firefish-js"; import MkTooltip from "./MkTooltip.vue"; import XReactionIcon from "@/components/MkReactionIcon.vue"; -import type { entities } from "firefish-js"; defineProps<{ showing: Ref<boolean>; diff --git a/packages/client/src/components/MkSignin.vue b/packages/client/src/components/MkSignin.vue index a49662d974..d2c1159642 100644 --- a/packages/client/src/components/MkSignin.vue +++ b/packages/client/src/components/MkSignin.vue @@ -136,6 +136,7 @@ <script lang="ts" setup> import { defineAsyncComponent, ref } from "vue"; import { toUnicode } from "punycode/"; +import type { entities } from "firefish-js"; import MkButton from "@/components/MkButton.vue"; import MkInput from "@/components/form/input.vue"; import MkInfo from "@/components/MkInfo.vue"; @@ -145,7 +146,6 @@ import * as os from "@/os"; import { signIn } from "@/account"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; -import type { entities } from "firefish-js"; const signing = ref(false); const user = ref<entities.UserDetailed | null>(null); diff --git a/packages/client/src/components/MkUserList.vue b/packages/client/src/components/MkUserList.vue index 5043b920ff..a9606c69d7 100644 --- a/packages/client/src/components/MkUserList.vue +++ b/packages/client/src/components/MkUserList.vue @@ -26,6 +26,7 @@ <script lang="ts" setup> import { ref } from "vue"; +import type { entities } from "firefish-js"; import MkUserInfo from "@/components/MkUserInfo.vue"; import type { MkPaginationType, @@ -34,7 +35,6 @@ import type { } from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue"; import { i18n } from "@/i18n"; -import type { entities } from "firefish-js"; defineProps<{ pagination: PagingOf<entities.UserDetailed>; diff --git a/packages/client/src/components/MkUsersTooltip.vue b/packages/client/src/components/MkUsersTooltip.vue index 741194221d..30373c6eb3 100644 --- a/packages/client/src/components/MkUsersTooltip.vue +++ b/packages/client/src/components/MkUsersTooltip.vue @@ -3,8 +3,8 @@ ref="tooltip" :target-element="targetElement" :max-width="250" - @closed="emit('closed')" :showing="showing" + @closed="emit('closed')" > <div class="beaffaef"> <div v-for="u in users" :key="u.id" class="user"> @@ -20,8 +20,8 @@ <script lang="ts" setup> import type { Ref } from "vue"; -import MkTooltip from "./MkTooltip.vue"; import type { entities } from "firefish-js"; +import MkTooltip from "./MkTooltip.vue"; defineProps<{ showing: Ref<boolean>; diff --git a/packages/client/src/components/MkWaitingDialog.vue b/packages/client/src/components/MkWaitingDialog.vue index 18cec42a49..f2d7688f36 100644 --- a/packages/client/src/components/MkWaitingDialog.vue +++ b/packages/client/src/components/MkWaitingDialog.vue @@ -29,7 +29,8 @@ </template> <script lang="ts" setup> -import { MaybeRef, shallowRef, watch, unref } from "vue"; +import type { MaybeRef } from "vue"; +import { shallowRef, unref, watch } from "vue"; import MkModal from "@/components/MkModal.vue"; import iconify from "@/scripts/icon"; diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 8fbe8b8041..eb4502bf9f 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -1,7 +1,7 @@ // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する import { EventEmitter } from "eventemitter3"; -import { type entities, api as firefishApi, type Endpoints } from "firefish-js"; +import { type Endpoints, type entities, api as firefishApi } from "firefish-js"; import insertTextAtCursor from "insert-text-at-cursor"; import type { Component, Ref } from "vue"; import { defineAsyncComponent, markRaw, ref } from "vue"; @@ -176,12 +176,12 @@ export function promiseDialog<T>( let popupIdCount = 0; -type PopupType = { +interface PopupType { id: number; component: Component; props: Record<string, unknown>; events: Record<string, unknown>; -}; +} export const popups = ref<PopupType[]>([]); const zIndexes = { diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue index 72850ed478..772e3f6f55 100644 --- a/packages/client/src/pages/admin/abuses.vue +++ b/packages/client/src/pages/admin/abuses.vue @@ -93,6 +93,7 @@ <script lang="ts" setup> import { computed, ref } from "vue"; +import type { entities } from "firefish-js"; import MkSelect from "@/components/form/select.vue"; import MkPagination, { type MkPaginationType, @@ -101,7 +102,6 @@ import XAbuseReport from "@/components/MkAbuseReport.vue"; import { i18n } from "@/i18n"; import { definePageMetadata } from "@/scripts/page-metadata"; import icon from "@/scripts/icon"; -import type { entities } from "firefish-js"; const reports = ref<MkPaginationType<typeof pagination.endpoint> | null>(null); diff --git a/packages/client/src/pages/channel-editor.vue b/packages/client/src/pages/channel-editor.vue index 3791a98d05..e776de30e3 100644 --- a/packages/client/src/pages/channel-editor.vue +++ b/packages/client/src/pages/channel-editor.vue @@ -41,6 +41,7 @@ <script lang="ts" setup> import { computed, ref, watch } from "vue"; +import type { entities } from "firefish-js"; import MkTextarea from "@/components/form/textarea.vue"; import MkButton from "@/components/MkButton.vue"; import MkInput from "@/components/form/input.vue"; @@ -50,7 +51,6 @@ import { useRouter } from "@/router"; import { definePageMetadata } from "@/scripts/page-metadata"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; -import type { entities } from "firefish-js"; const router = useRouter(); diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue index b8ec491ecb..d3ef5f2b8b 100644 --- a/packages/client/src/pages/channel.vue +++ b/packages/client/src/pages/channel.vue @@ -96,6 +96,7 @@ <script lang="ts" setup> import { computed, ref, watch } from "vue"; +import type { entities } from "firefish-js"; import XPostForm from "@/components/MkPostForm.vue"; import XTimeline from "@/components/MkTimeline.vue"; import XChannelFollowButton from "@/components/MkChannelFollowButton.vue"; @@ -105,7 +106,6 @@ import { me } from "@/me"; import { i18n } from "@/i18n"; import { definePageMetadata } from "@/scripts/page-metadata"; import icon from "@/scripts/icon"; -import type { entities } from "firefish-js"; const router = useRouter(); diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue index 5286f909d4..c7789b8787 100644 --- a/packages/client/src/pages/channels.vue +++ b/packages/client/src/pages/channels.vue @@ -112,6 +112,7 @@ import { computed, onMounted, ref, watch } from "vue"; import { Virtual } from "swiper/modules"; import { Swiper, SwiperSlide } from "swiper/vue"; +import type { Swiper as SwiperType } from "swiper/types"; import MkChannelList from "@/components/MkChannelList.vue"; import MkInput from "@/components/form/input.vue"; import MkRadios from "@/components/form/radios.vue"; @@ -125,7 +126,6 @@ import { defaultStore } from "@/store"; import icon from "@/scripts/icon"; import "swiper/scss"; import "swiper/scss/virtual"; -import type { Swiper as SwiperType } from "swiper/types"; const router = useRouter(); diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue index f8c92c1867..91b6b764c5 100644 --- a/packages/client/src/pages/gallery/post.vue +++ b/packages/client/src/pages/gallery/post.vue @@ -150,6 +150,7 @@ <script lang="ts" setup> import { computed, ref, watch } from "vue"; +import type { entities } from "firefish-js"; import MkButton from "@/components/MkButton.vue"; import * as os from "@/os"; import MkContainer from "@/components/MkContainer.vue"; @@ -164,7 +165,6 @@ import { shareAvailable } from "@/scripts/share-available"; import { defaultStore } from "@/store"; import icon from "@/scripts/icon"; import { isSignedIn, me } from "@/me"; -import type { entities } from "firefish-js"; const router = useRouter(); diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue index 5b645479a9..ef45ad75f7 100644 --- a/packages/client/src/pages/my-antennas/index.vue +++ b/packages/client/src/pages/my-antennas/index.vue @@ -54,7 +54,8 @@ <script lang="ts" setup> import { computed, onActivated, onDeactivated, ref } from "vue"; -import MkPagination, { MkPaginationType } from "@/components/MkPagination.vue"; +import type { MkPaginationType } from "@/components/MkPagination.vue"; +import MkPagination from "@/components/MkPagination.vue"; import MkButton from "@/components/MkButton.vue"; import MkInfo from "@/components/MkInfo.vue"; import { i18n } from "@/i18n"; diff --git a/packages/client/src/pages/note-history.vue b/packages/client/src/pages/note-history.vue index 8bc7f225c8..d0c93899aa 100644 --- a/packages/client/src/pages/note-history.vue +++ b/packages/client/src/pages/note-history.vue @@ -34,10 +34,10 @@ <script lang="ts" setup> import { computed, onMounted, ref } from "vue"; +import type { entities } from "firefish-js"; import MkPagination, { type MkPaginationType, } from "@/components/MkPagination.vue"; -import type { entities } from "firefish-js"; import { api } from "@/os"; import XList from "@/components/MkDateSeparatedList.vue"; import XNote from "@/components/MkNote.vue"; diff --git a/packages/client/src/scripts/use-chart-tooltip.ts b/packages/client/src/scripts/use-chart-tooltip.ts index bc6c5b1eeb..62347ede8d 100644 --- a/packages/client/src/scripts/use-chart-tooltip.ts +++ b/packages/client/src/scripts/use-chart-tooltip.ts @@ -1,13 +1,13 @@ import { onDeactivated, onUnmounted, ref } from "vue"; +import type { Color, TooltipOptions } from "chart.js"; import * as os from "@/os"; import MkChartTooltip from "@/components/MkChartTooltip.vue"; -import type { Color, TooltipOptions } from "chart.js"; -type ToolTipSerie = { +interface ToolTipSerie { backgroundColor: Color; borderColor: Color; text: string; -}; +} export function useChartTooltip( opts: { position: "top" | "middle" } = { position: "top" }, diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index 680dba286f..29393123c0 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -1,8 +1,8 @@ import { markRaw, ref } from "vue"; +import type { ApiTypes, entities } from "firefish-js"; import { isSignedIn } from "./me"; import { Storage } from "./pizzax"; import type { NoteVisibility } from "@/types/note"; -import type { entities, ApiTypes } from "firefish-js"; export const postFormActions: { title: string; diff --git a/packages/client/src/types/form.ts b/packages/client/src/types/form.ts index 88f90ebbef..c5e169c465 100644 --- a/packages/client/src/types/form.ts +++ b/packages/client/src/types/form.ts @@ -1,9 +1,9 @@ -export type BaseFormItem = { +export interface BaseFormItem { hidden?: boolean; label?: string; description?: string; required?: boolean; -}; +} export type FormItemTextInput = BaseFormItem & { type: "string"; diff --git a/packages/client/src/types/note.ts b/packages/client/src/types/note.ts index 7f4de74d77..17338c02b8 100644 --- a/packages/client/src/types/note.ts +++ b/packages/client/src/types/note.ts @@ -2,7 +2,7 @@ import type { noteVisibilities } from "firefish-js"; export type NoteVisibility = (typeof noteVisibilities)[number] | "private"; -export type NoteTranslation = { +export interface NoteTranslation { sourceLang: string; text: string; -}; +} diff --git a/packages/client/src/types/post-form.ts b/packages/client/src/types/post-form.ts index a535ef812f..c8d92f4636 100644 --- a/packages/client/src/types/post-form.ts +++ b/packages/client/src/types/post-form.ts @@ -1,11 +1,11 @@ import type { entities } from "firefish-js"; -export type PollType = { +export interface PollType { choices: string[]; multiple: boolean; expiresAt: string | null; expiredAfter: number | null; -}; +} export type NoteDraft = entities.Note & { poll?: PollType; From c6e27762983bd8140adfe4cbbe93dc96c07113ef Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Fri, 19 Apr 2024 03:42:49 +0900 Subject: [PATCH 085/110] chore (backend): remove a horrible and unused function --- packages/backend/src/db/postgre.ts | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index 19f7e7816a..6baccaa271 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -237,31 +237,3 @@ export async function initDb(force = false) { await db.initialize(); } } - -export async function resetDb() { - const reset = async () => { - await redisClient.flushdb(); - const tables = await db.query(`SELECT relname AS "table" - FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) - WHERE nspname NOT IN ('pg_catalog', 'information_schema') - AND C.relkind = 'r' - AND nspname !~ '^pg_toast';`); - for (const table of tables) { - await db.query(`DELETE FROM "${table.table}" CASCADE`); - } - }; - - for (let i = 1; i <= 3; i++) { - try { - await reset(); - } catch (e) { - if (i === 3) { - throw e; - } else { - await new Promise((resolve) => setTimeout(resolve, 1000)); - continue; - } - } - break; - } -} From 4823abd3a9415cea56f703854e8ea344a65f644b Mon Sep 17 00:00:00 2001 From: yumeko <yumeko@mainichi.social> Date: Fri, 19 Apr 2024 03:34:25 +0300 Subject: [PATCH 086/110] Add usageHint field to DriveFile, and fill accordingly when operating on Persons --- .../migration/1713451569342-AddDriveFileUsage.ts | 15 +++++++++++++++ .../backend/src/models/entities/drive-file.ts | 7 +++++++ .../src/remote/activitypub/models/image.ts | 5 ++++- .../backend/src/remote/activitypub/models/note.ts | 4 ++-- .../src/remote/activitypub/models/person.ts | 8 ++++---- packages/backend/src/services/drive/add-file.ts | 10 ++++++++++ .../backend/src/services/drive/upload-from-url.ts | 11 +++++++++-- 7 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts diff --git a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts new file mode 100644 index 0000000000..c0c96fe74c --- /dev/null +++ b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts @@ -0,0 +1,15 @@ +import type { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddDriveFileUsage1713451569342 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query( + `ALTER TABLE "drive_file" ADD "usageHint" character varying(16) DEFAULT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query( + `ALTER TABLE "drive_file" DROP COLUMN "usageHint"` + ); + } +} diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 3c49e89fd5..b5717d62cd 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -177,6 +177,13 @@ export class DriveFile { }) public isSensitive: boolean; + @Column("varchar", { + length: 16, + nullable: true, + comment: "Hint for what the file is used for.", + }) + public usageHint: string | null; + /** * 外部の(信頼されていない)URLへの直リンクか否か */ diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts index e2072a963a..23fb720362 100644 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ b/packages/backend/src/remote/activitypub/models/image.ts @@ -16,6 +16,7 @@ const logger = apLogger; export async function createImage( actor: CacheableRemoteUser, value: any, + usage: 'avatar' | 'banner' | null ): Promise<DriveFile> { // Skip if author is frozen. if (actor.isSuspended) { @@ -43,6 +44,7 @@ export async function createImage( sensitive: image.sensitive, isLink: !instance.cacheRemoteFiles, comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH), + usageHint: usage }); if (file.isLink) { @@ -73,9 +75,10 @@ export async function createImage( export async function resolveImage( actor: CacheableRemoteUser, value: any, + usage: 'avatar' | 'banner' | null, ): Promise<DriveFile> { // TODO // Fetch from remote server and register - return await createImage(actor, value); + return await createImage(actor, value, usage); } diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index ad59930457..5e1c3829a7 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -213,7 +213,7 @@ export async function createNote( ? ( await Promise.all( note.attachment.map( - (x) => limit(() => resolveImage(actor, x)) as Promise<DriveFile>, + (x) => limit(() => resolveImage(actor, x, null)) as Promise<DriveFile>, ), ) ).filter((image) => image != null) @@ -616,7 +616,7 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) { fileList.map( (x) => limit(async () => { - const file = await resolveImage(actor, x); + const file = await resolveImage(actor, x, null); const update: Partial<DriveFile> = {}; const altText = truncate(x.name, DB_MAX_IMAGE_COMMENT_LENGTH); diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index e91280125f..47a1152c36 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -362,10 +362,10 @@ export async function createPerson( //#region Fetch avatar and header image const [avatar, banner] = await Promise.all( - [person.icon, person.image].map((img) => + [person.icon, person.image].map((img, index) => img == null ? Promise.resolve(null) - : resolveImage(user!, img).catch(() => null), + : resolveImage(user!, img, index === 0 ? "avatar" : index === 1 ? "banner" : null).catch(() => null), ), ); @@ -438,10 +438,10 @@ export async function updatePerson( // Fetch avatar and header image const [avatar, banner] = await Promise.all( - [person.icon, person.image].map((img) => + [person.icon, person.image].map((img, index) => img == null ? Promise.resolve(null) - : resolveImage(user, img).catch(() => null), + : resolveImage(user, img, index === 0 ? "avatar" : index === 1 ? "banner" : null).catch(() => null), ), ); diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 24ad9f8f02..cfb871dd93 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -65,6 +65,7 @@ function urlPathJoin( * @param type Content-Type for original * @param hash Hash for original * @param size Size for original + * @param usage Optional usage hint for file (f.e. "avatar") */ async function save( file: DriveFile, @@ -73,6 +74,7 @@ async function save( type: string, hash: string, size: number, + usage: string | null = null ): Promise<DriveFile> { // thunbnail, webpublic を必要なら生成 const alts = await generateAlts(path, type, !file.uri); @@ -161,6 +163,7 @@ async function save( file.md5 = hash; file.size = size; file.storedInternal = false; + file.usageHint = usage ?? null; return await DriveFiles.insert(file).then((x) => DriveFiles.findOneByOrFail(x.identifiers[0]), @@ -204,6 +207,7 @@ async function save( file.type = type; file.md5 = hash; file.size = size; + file.usageHint = usage ?? null; return await DriveFiles.insert(file).then((x) => DriveFiles.findOneByOrFail(x.identifiers[0]), @@ -450,6 +454,9 @@ type AddFileArgs = { requestIp?: string | null; requestHeaders?: Record<string, string> | null; + + /** Whether this file has a known use case, like user avatar or instance icon */ + usageHint?: string | null; }; /** @@ -469,6 +476,7 @@ export async function addFile({ sensitive = null, requestIp = null, requestHeaders = null, + usageHint = null, }: AddFileArgs): Promise<DriveFile> { const info = await getFileInfo(path); logger.info(`${JSON.stringify(info)}`); @@ -581,6 +589,7 @@ export async function addFile({ file.isLink = isLink; file.requestIp = requestIp; file.requestHeaders = requestHeaders; + file.usageHint = usageHint; file.isSensitive = user ? Users.isLocalUser(user) && (instance!.markLocalFilesNsfwByDefault || profile!.alwaysMarkNsfw) @@ -639,6 +648,7 @@ export async function addFile({ info.type.mime, info.md5, info.size, + usageHint ); } diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts index 551d3757ca..238f8714fa 100644 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ b/packages/backend/src/services/drive/upload-from-url.ts @@ -13,7 +13,11 @@ const logger = driveLogger.createSubLogger("downloader"); type Args = { url: string; - user: { id: User["id"]; host: User["host"] } | null; + user: { + id: User["id"]; + host: User["host"]; + driveCapacityOverrideMb: User["driveCapacityOverrideMb"]; + } | null; folderId?: DriveFolder["id"] | null; uri?: string | null; sensitive?: boolean; @@ -22,6 +26,7 @@ type Args = { comment?: string | null; requestIp?: string | null; requestHeaders?: Record<string, string> | null; + usageHint?: string | null; }; export async function uploadFromUrl({ @@ -35,6 +40,7 @@ export async function uploadFromUrl({ comment = null, requestIp = null, requestHeaders = null, + usageHint = null }: Args): Promise<DriveFile> { const parsedUrl = new URL(url); if ( @@ -75,9 +81,10 @@ export async function uploadFromUrl({ sensitive, requestIp, requestHeaders, + usageHint }); logger.succ(`Got: ${driveFile.id}`); - return driveFile!; + return driveFile; } catch (e) { logger.error(`Failed to create drive file:\n${inspect(e)}`); throw e; From c0f93de94b2183c53ce041b58d028474c0f7ed73 Mon Sep 17 00:00:00 2001 From: yumeko <yumeko@mainichi.social> Date: Fri, 19 Apr 2024 06:26:40 +0300 Subject: [PATCH 087/110] Set file usage hints on local avatar/banner uploads as well + export "valid" values as type --- .../backend/src/models/entities/drive-file.ts | 4 +++- .../src/remote/activitypub/models/image.ts | 6 ++--- .../src/remote/activitypub/models/person.ts | 4 ++-- .../src/server/api/endpoints/i/update.ts | 23 +++++++++++++++++-- .../backend/src/services/drive/add-file.ts | 7 +++--- 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index b5717d62cd..f257280d60 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -16,6 +16,8 @@ import { DriveFolder } from "./drive-folder.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; import { NoteFile } from "./note-file.js"; +export type DriveFileUsageHint = "user_avatar" | "user_banner" | null; + @Entity() @Index(["userId", "folderId", "id"]) export class DriveFile { @@ -182,7 +184,7 @@ export class DriveFile { nullable: true, comment: "Hint for what the file is used for.", }) - public usageHint: string | null; + public usageHint: DriveFileUsageHint; /** * 外部の(信頼されていない)URLへの直リンクか否か diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts index 23fb720362..9183b82ebe 100644 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ b/packages/backend/src/remote/activitypub/models/image.ts @@ -3,7 +3,7 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js"; import Resolver from "../resolver.js"; import { fetchMeta } from "backend-rs"; import { apLogger } from "../logger.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; +import type { DriveFile, DriveFileUsageHint } from "@/models/entities/drive-file.js"; import { DriveFiles } from "@/models/index.js"; import { truncate } from "@/misc/truncate.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; @@ -16,7 +16,7 @@ const logger = apLogger; export async function createImage( actor: CacheableRemoteUser, value: any, - usage: 'avatar' | 'banner' | null + usage: DriveFileUsageHint ): Promise<DriveFile> { // Skip if author is frozen. if (actor.isSuspended) { @@ -75,7 +75,7 @@ export async function createImage( export async function resolveImage( actor: CacheableRemoteUser, value: any, - usage: 'avatar' | 'banner' | null, + usage: DriveFileUsageHint, ): Promise<DriveFile> { // TODO diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 47a1152c36..64a4c7f5c5 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -365,7 +365,7 @@ export async function createPerson( [person.icon, person.image].map((img, index) => img == null ? Promise.resolve(null) - : resolveImage(user!, img, index === 0 ? "avatar" : index === 1 ? "banner" : null).catch(() => null), + : resolveImage(user, img, index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null).catch(() => null) ), ); @@ -441,7 +441,7 @@ export async function updatePerson( [person.icon, person.image].map((img, index) => img == null ? Promise.resolve(null) - : resolveImage(user, img, index === 0 ? "avatar" : index === 1 ? "banner" : null).catch(() => null), + : resolveImage(user, img, index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null).catch(() => null), ), ); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 4389688a12..9d39903eac 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -13,6 +13,7 @@ import { normalizeForSearch } from "@/misc/normalize-for-search.js"; import { verifyLink } from "@/services/fetch-rel-me.js"; import { ApiError } from "@/server/api/error.js"; import define from "@/server/api/define.js"; +import { DriveFile } from "@/models/entities/drive-file"; export const meta = { tags: ["account"], @@ -241,8 +242,9 @@ export default define(meta, paramDef, async (ps, _user, token) => { if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; + let avatar: DriveFile | null = null if (ps.avatarId) { - const avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); + avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); @@ -250,8 +252,9 @@ export default define(meta, paramDef, async (ps, _user, token) => { throw new ApiError(meta.errors.avatarNotAnImage); } + let banner: DriveFile | null = null if (ps.bannerId) { - const banner = await DriveFiles.findOneBy({ id: ps.bannerId }); + banner = await DriveFiles.findOneBy({ id: ps.bannerId }); if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); @@ -328,6 +331,22 @@ export default define(meta, paramDef, async (ps, _user, token) => { updateUsertags(user, tags); //#endregion + // Update old/new avatar usage hints + if (avatar) + { + if (user.avatarId) + await DriveFiles.update(user.avatarId, {usageHint: null}); + await DriveFiles.update(avatar.id, {usageHint: "user_avatar"}); + } + + // Update old/new banner usage hints + if (banner) + { + if (user.bannerId) + await DriveFiles.update(user.bannerId, {usageHint: null}); + await DriveFiles.update(banner.id, {usageHint: "user_banner"}); + } + if (Object.keys(updates).length > 0) await Users.update(user.id, updates); if (Object.keys(profileUpdates).length > 0) await UserProfiles.update(user.id, profileUpdates); diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index cfb871dd93..79f30a651b 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -16,6 +16,7 @@ import { UserProfiles, } from "@/models/index.js"; import { DriveFile } from "@/models/entities/drive-file.js"; +import type { DriveFileUsageHint } from "@/models/entities/drive-file.js"; import type { IRemoteUser, User } from "@/models/entities/user.js"; import { genId } from "backend-rs"; import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; @@ -65,7 +66,7 @@ function urlPathJoin( * @param type Content-Type for original * @param hash Hash for original * @param size Size for original - * @param usage Optional usage hint for file (f.e. "avatar") + * @param usage Optional usage hint for file (f.e. "user_avatar") */ async function save( file: DriveFile, @@ -74,7 +75,7 @@ async function save( type: string, hash: string, size: number, - usage: string | null = null + usage: DriveFileUsageHint = null ): Promise<DriveFile> { // thunbnail, webpublic を必要なら生成 const alts = await generateAlts(path, type, !file.uri); @@ -456,7 +457,7 @@ type AddFileArgs = { requestHeaders?: Record<string, string> | null; /** Whether this file has a known use case, like user avatar or instance icon */ - usageHint?: string | null; + usageHint?: DriveFileUsageHint; }; /** From 4aeb0d95ccf7e4427c60ecd915a48922f465e451 Mon Sep 17 00:00:00 2001 From: yumeko <yumeko@mainichi.social> Date: Fri, 19 Apr 2024 07:03:09 +0300 Subject: [PATCH 088/110] Add DriveFile usageHint field to rust model as well --- packages/backend-rs/src/model/entity/drive_file.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend-rs/src/model/entity/drive_file.rs b/packages/backend-rs/src/model/entity/drive_file.rs index e3e4622a62..e5c3995573 100644 --- a/packages/backend-rs/src/model/entity/drive_file.rs +++ b/packages/backend-rs/src/model/entity/drive_file.rs @@ -52,6 +52,8 @@ pub struct Model { pub request_headers: Option<Json>, #[sea_orm(column_name = "requestIp")] pub request_ip: Option<String>, + #[sea_orm(column_name = "usageHint")] + pub usage_hint: Option<String>, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] From 913de651dbb4413728599a1c7df3a646098c750c Mon Sep 17 00:00:00 2001 From: yumeko <yumeko@mainichi.social> Date: Fri, 19 Apr 2024 07:25:42 +0300 Subject: [PATCH 089/110] When updating (remote) user avatar/banner, clear usageHint for the previous drivefile, if any --- packages/backend/src/remote/activitypub/models/person.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 64a4c7f5c5..5460a0234b 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -10,6 +10,7 @@ import { Followings, UserProfiles, UserPublickeys, + DriveFiles, } from "@/models/index.js"; import type { IRemoteUser, CacheableUser } from "@/models/entities/user.js"; import { User } from "@/models/entities/user.js"; @@ -561,10 +562,14 @@ export async function updatePerson( } as Partial<User>; if (avatar) { + if (user?.avatarId) + await DriveFiles.update(user.avatarId, {usageHint: null}); updates.avatarId = avatar.id; } if (banner) { + if (user?.bannerId) + await DriveFiles.update(user.bannerId, {usageHint: null}); updates.bannerId = banner.id; } From 968657d26eff412fd18e8b8cab47718f40a0f5e1 Mon Sep 17 00:00:00 2001 From: yumeko <yumeko@mainichi.social> Date: Fri, 19 Apr 2024 07:54:11 +0300 Subject: [PATCH 090/110] Run format --- .../1713451569342-AddDriveFileUsage.ts | 4 +--- .../src/remote/activitypub/models/image.ts | 9 ++++++--- .../src/remote/activitypub/models/note.ts | 3 ++- .../src/remote/activitypub/models/person.ts | 16 ++++++++++++---- .../src/server/api/endpoints/i/update.ts | 18 ++++++++---------- .../backend/src/services/drive/add-file.ts | 4 ++-- .../src/services/drive/upload-from-url.ts | 8 ++++---- 7 files changed, 35 insertions(+), 27 deletions(-) diff --git a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts index c0c96fe74c..57c6a73890 100644 --- a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts +++ b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts @@ -8,8 +8,6 @@ export class AddDriveFileUsage1713451569342 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query( - `ALTER TABLE "drive_file" DROP COLUMN "usageHint"` - ); + await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "usageHint"`); } } diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts index 9183b82ebe..a6ac698feb 100644 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ b/packages/backend/src/remote/activitypub/models/image.ts @@ -3,7 +3,10 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js"; import Resolver from "../resolver.js"; import { fetchMeta } from "backend-rs"; import { apLogger } from "../logger.js"; -import type { DriveFile, DriveFileUsageHint } from "@/models/entities/drive-file.js"; +import type { + DriveFile, + DriveFileUsageHint, +} from "@/models/entities/drive-file.js"; import { DriveFiles } from "@/models/index.js"; import { truncate } from "@/misc/truncate.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; @@ -16,7 +19,7 @@ const logger = apLogger; export async function createImage( actor: CacheableRemoteUser, value: any, - usage: DriveFileUsageHint + usage: DriveFileUsageHint, ): Promise<DriveFile> { // Skip if author is frozen. if (actor.isSuspended) { @@ -44,7 +47,7 @@ export async function createImage( sensitive: image.sensitive, isLink: !instance.cacheRemoteFiles, comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH), - usageHint: usage + usageHint: usage, }); if (file.isLink) { diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 5e1c3829a7..b2fd67288c 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -213,7 +213,8 @@ export async function createNote( ? ( await Promise.all( note.attachment.map( - (x) => limit(() => resolveImage(actor, x, null)) as Promise<DriveFile>, + (x) => + limit(() => resolveImage(actor, x, null)) as Promise<DriveFile>, ), ) ).filter((image) => image != null) diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 5460a0234b..657ac7d553 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -366,7 +366,11 @@ export async function createPerson( [person.icon, person.image].map((img, index) => img == null ? Promise.resolve(null) - : resolveImage(user, img, index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null).catch(() => null) + : resolveImage( + user, + img, + index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null, + ).catch(() => null), ), ); @@ -442,7 +446,11 @@ export async function updatePerson( [person.icon, person.image].map((img, index) => img == null ? Promise.resolve(null) - : resolveImage(user, img, index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null).catch(() => null), + : resolveImage( + user, + img, + index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null, + ).catch(() => null), ), ); @@ -563,13 +571,13 @@ export async function updatePerson( if (avatar) { if (user?.avatarId) - await DriveFiles.update(user.avatarId, {usageHint: null}); + await DriveFiles.update(user.avatarId, { usageHint: null }); updates.avatarId = avatar.id; } if (banner) { if (user?.bannerId) - await DriveFiles.update(user.bannerId, {usageHint: null}); + await DriveFiles.update(user.bannerId, { usageHint: null }); updates.bannerId = banner.id; } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 9d39903eac..29d03cc465 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -242,7 +242,7 @@ export default define(meta, paramDef, async (ps, _user, token) => { if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; - let avatar: DriveFile | null = null + let avatar: DriveFile | null = null; if (ps.avatarId) { avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); @@ -252,7 +252,7 @@ export default define(meta, paramDef, async (ps, _user, token) => { throw new ApiError(meta.errors.avatarNotAnImage); } - let banner: DriveFile | null = null + let banner: DriveFile | null = null; if (ps.bannerId) { banner = await DriveFiles.findOneBy({ id: ps.bannerId }); @@ -332,19 +332,17 @@ export default define(meta, paramDef, async (ps, _user, token) => { //#endregion // Update old/new avatar usage hints - if (avatar) - { + if (avatar) { if (user.avatarId) - await DriveFiles.update(user.avatarId, {usageHint: null}); - await DriveFiles.update(avatar.id, {usageHint: "user_avatar"}); + await DriveFiles.update(user.avatarId, { usageHint: null }); + await DriveFiles.update(avatar.id, { usageHint: "user_avatar" }); } // Update old/new banner usage hints - if (banner) - { + if (banner) { if (user.bannerId) - await DriveFiles.update(user.bannerId, {usageHint: null}); - await DriveFiles.update(banner.id, {usageHint: "user_banner"}); + await DriveFiles.update(user.bannerId, { usageHint: null }); + await DriveFiles.update(banner.id, { usageHint: "user_banner" }); } if (Object.keys(updates).length > 0) await Users.update(user.id, updates); diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 79f30a651b..2f975e1984 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -75,7 +75,7 @@ async function save( type: string, hash: string, size: number, - usage: DriveFileUsageHint = null + usage: DriveFileUsageHint = null, ): Promise<DriveFile> { // thunbnail, webpublic を必要なら生成 const alts = await generateAlts(path, type, !file.uri); @@ -649,7 +649,7 @@ export async function addFile({ info.type.mime, info.md5, info.size, - usageHint + usageHint, ); } diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts index 238f8714fa..a96e8e3262 100644 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ b/packages/backend/src/services/drive/upload-from-url.ts @@ -3,7 +3,7 @@ import type { User } from "@/models/entities/user.js"; import { createTemp } from "@/misc/create-temp.js"; import { downloadUrl, isPrivateIp } from "@/misc/download-url.js"; import type { DriveFolder } from "@/models/entities/drive-folder.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; +import type { DriveFile, DriveFileUsageHint } from "@/models/entities/drive-file.js"; import { DriveFiles } from "@/models/index.js"; import { driveLogger } from "./logger.js"; import { addFile } from "./add-file.js"; @@ -26,7 +26,7 @@ type Args = { comment?: string | null; requestIp?: string | null; requestHeaders?: Record<string, string> | null; - usageHint?: string | null; + usageHint?: DriveFileUsageHint; }; export async function uploadFromUrl({ @@ -40,7 +40,7 @@ export async function uploadFromUrl({ comment = null, requestIp = null, requestHeaders = null, - usageHint = null + usageHint = null, }: Args): Promise<DriveFile> { const parsedUrl = new URL(url); if ( @@ -81,7 +81,7 @@ export async function uploadFromUrl({ sensitive, requestIp, requestHeaders, - usageHint + usageHint, }); logger.succ(`Got: ${driveFile.id}`); return driveFile; From 1be5373dfc839be589668dea84009eb33fa0213b Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Fri, 19 Apr 2024 21:59:12 +0900 Subject: [PATCH 091/110] chore (backend-rs): make exported enum compatible w/ TypeScript's string enum --- packages/backend-rs/Makefile | 2 +- packages/backend-rs/index.d.ts | 132 +++++++++--------- packages/backend-rs/package.json | 4 +- .../src/model/entity/sea_orm_active_enums.rs | 20 +-- 4 files changed, 79 insertions(+), 79 deletions(-) diff --git a/packages/backend-rs/Makefile b/packages/backend-rs/Makefile index eb4b30c6df..11b614c82a 100644 --- a/packages/backend-rs/Makefile +++ b/packages/backend-rs/Makefile @@ -17,7 +17,7 @@ regenerate-entities: attribute=$$(printf 'cfg_attr(feature = "napi", napi_derive::napi(object, js_name = "%s", use_nullable = true))' "$${jsname}"); \ sed -i "s/NAPI_EXTRA_ATTR_PLACEHOLDER/$${attribute}/" "$${file}"; \ done - sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi)]/' \ + sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]/' \ src/model/entity/sea_orm_active_enums.rs cargo fmt --all -- diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 5ee69969c6..1cf961bd30 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -772,81 +772,81 @@ export interface ReplyMuting { muteeId: string muterId: string } -export const enum AntennaSrcEnum { - All = 0, - Group = 1, - Home = 2, - Instances = 3, - List = 4, - Users = 5 +export enum AntennaSrcEnum { + All = 'all', + Group = 'group', + Home = 'home', + Instances = 'instances', + List = 'list', + Users = 'users' } -export const enum MutedNoteReasonEnum { - Manual = 0, - Other = 1, - Spam = 2, - Word = 3 +export enum MutedNoteReasonEnum { + Manual = 'manual', + Other = 'other', + Spam = 'spam', + Word = 'word' } -export const enum NoteVisibilityEnum { - Followers = 0, - Hidden = 1, - Home = 2, - Public = 3, - Specified = 4 +export enum NoteVisibilityEnum { + Followers = 'followers', + Hidden = 'hidden', + Home = 'home', + Public = 'public', + Specified = 'specified' } -export const enum NotificationTypeEnum { - App = 0, - Follow = 1, - FollowRequestAccepted = 2, - GroupInvited = 3, - Mention = 4, - PollEnded = 5, - PollVote = 6, - Quote = 7, - Reaction = 8, - ReceiveFollowRequest = 9, - Renote = 10, - Reply = 11 +export enum NotificationTypeEnum { + App = 'app', + Follow = 'follow', + FollowRequestAccepted = 'followRequestAccepted', + GroupInvited = 'groupInvited', + Mention = 'mention', + PollEnded = 'pollEnded', + PollVote = 'pollVote', + Quote = 'quote', + Reaction = 'reaction', + ReceiveFollowRequest = 'receiveFollowRequest', + Renote = 'renote', + Reply = 'reply' } -export const enum PageVisibilityEnum { - Followers = 0, - Public = 1, - Specified = 2 +export enum PageVisibilityEnum { + Followers = 'followers', + Public = 'public', + Specified = 'specified' } -export const enum PollNotevisibilityEnum { - Followers = 0, - Home = 1, - Public = 2, - Specified = 3 +export enum PollNotevisibilityEnum { + Followers = 'followers', + Home = 'home', + Public = 'public', + Specified = 'specified' } -export const enum RelayStatusEnum { - Accepted = 0, - Rejected = 1, - Requesting = 2 +export enum RelayStatusEnum { + Accepted = 'accepted', + Rejected = 'rejected', + Requesting = 'requesting' } -export const enum UserEmojimodpermEnum { - Add = 0, - Full = 1, - Mod = 2, - Unauthorized = 3 +export enum UserEmojimodpermEnum { + Add = 'add', + Full = 'full', + Mod = 'mod', + Unauthorized = 'unauthorized' } -export const enum UserProfileFfvisibilityEnum { - Followers = 0, - Private = 1, - Public = 2 +export enum UserProfileFfvisibilityEnum { + Followers = 'followers', + Private = 'private', + Public = 'public' } -export const enum UserProfileMutingnotificationtypesEnum { - App = 0, - Follow = 1, - FollowRequestAccepted = 2, - GroupInvited = 3, - Mention = 4, - PollEnded = 5, - PollVote = 6, - Quote = 7, - Reaction = 8, - ReceiveFollowRequest = 9, - Renote = 10, - Reply = 11 +export enum UserProfileMutingnotificationtypesEnum { + App = 'app', + Follow = 'follow', + FollowRequestAccepted = 'followRequestAccepted', + GroupInvited = 'groupInvited', + Mention = 'mention', + PollEnded = 'pollEnded', + PollVote = 'pollVote', + Quote = 'quote', + Reaction = 'reaction', + ReceiveFollowRequest = 'receiveFollowRequest', + Renote = 'renote', + Reply = 'reply' } export interface Signin { id: string diff --git a/packages/backend-rs/package.json b/packages/backend-rs/package.json index 69ae09fa49..1f3f49e9fb 100644 --- a/packages/backend-rs/package.json +++ b/packages/backend-rs/package.json @@ -33,8 +33,8 @@ }, "scripts": { "artifacts": "napi artifacts", - "build": "napi build --features napi --platform --release ./built/", - "build:debug": "napi build --features napi --platform ./built/", + "build": "napi build --features napi --no-const-enum --platform --release ./built/", + "build:debug": "napi build --features napi --no-const-enum --platform ./built/", "prepublishOnly": "napi prepublish -t npm", "test": "pnpm run cargo:test && pnpm run build:debug && ava", "universal": "napi universal", diff --git a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs index 861b3a18d0..38820e1bd8 100644 --- a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs +++ b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs @@ -4,7 +4,7 @@ use sea_orm::entity::prelude::*; #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "antenna_src_enum")] pub enum AntennaSrcEnum { #[sea_orm(string_value = "all")] @@ -22,7 +22,7 @@ pub enum AntennaSrcEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -40,7 +40,7 @@ pub enum MutedNoteReasonEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -60,7 +60,7 @@ pub enum NoteVisibilityEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -94,7 +94,7 @@ pub enum NotificationTypeEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -110,7 +110,7 @@ pub enum PageVisibilityEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -128,7 +128,7 @@ pub enum PollNotevisibilityEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "relay_status_enum")] pub enum RelayStatusEnum { #[sea_orm(string_value = "accepted")] @@ -140,7 +140,7 @@ pub enum RelayStatusEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -158,7 +158,7 @@ pub enum UserEmojimodpermEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -174,7 +174,7 @@ pub enum UserProfileFfvisibilityEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", From 6c46bb56fd2c13672ac6338b970f090f1f3c0672 Mon Sep 17 00:00:00 2001 From: yumeko <yumeko@mainichi.social> Date: Fri, 19 Apr 2024 18:22:02 +0300 Subject: [PATCH 092/110] Switch DriveFile's usageHint field to an enum type --- .../src/migration/1713451569342-AddDriveFileUsage.ts | 6 +++++- packages/backend/src/models/entities/drive-file.ts | 7 ++++--- packages/backend/src/remote/activitypub/models/person.ts | 4 ++-- packages/backend/src/server/api/endpoints/i/update.ts | 4 ++-- packages/backend/src/services/drive/add-file.ts | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts index 57c6a73890..3bdb1aafc8 100644 --- a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts +++ b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts @@ -3,11 +3,15 @@ import type { MigrationInterface, QueryRunner } from "typeorm"; export class AddDriveFileUsage1713451569342 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query( - `ALTER TABLE "drive_file" ADD "usageHint" character varying(16) DEFAULT NULL`, + `CREATE TYPE drive_file_usage_hint_enum AS ENUM ('userAvatar', 'userBanner')`, + ); + await queryRunner.query( + `ALTER TABLE "drive_file" ADD "usageHint" drive_file_usage_hint_enum DEFAULT NULL`, ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "usageHint"`); + await queryRunner.query(`DROP TYPE drive_file_usage_hint_enum`); } } diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index f257280d60..2c6c1bf598 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -16,7 +16,7 @@ import { DriveFolder } from "./drive-folder.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; import { NoteFile } from "./note-file.js"; -export type DriveFileUsageHint = "user_avatar" | "user_banner" | null; +export type DriveFileUsageHint = "userAvatar" | "userBanner" | null; @Entity() @Index(["userId", "folderId", "id"]) @@ -179,8 +179,9 @@ export class DriveFile { }) public isSensitive: boolean; - @Column("varchar", { - length: 16, + @Column({ + type: "enum", + enum: ["userAvatar", "userBanner"], nullable: true, comment: "Hint for what the file is used for.", }) diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 657ac7d553..4baa2c021b 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -369,7 +369,7 @@ export async function createPerson( : resolveImage( user, img, - index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null, + index === 0 ? "userAvatar" : index === 1 ? "userBanner" : null, ).catch(() => null), ), ); @@ -449,7 +449,7 @@ export async function updatePerson( : resolveImage( user, img, - index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null, + index === 0 ? "userAvatar" : index === 1 ? "userBanner" : null, ).catch(() => null), ), ); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 29d03cc465..4f65c59a9e 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -335,14 +335,14 @@ export default define(meta, paramDef, async (ps, _user, token) => { if (avatar) { if (user.avatarId) await DriveFiles.update(user.avatarId, { usageHint: null }); - await DriveFiles.update(avatar.id, { usageHint: "user_avatar" }); + await DriveFiles.update(avatar.id, { usageHint: "userAvatar" }); } // Update old/new banner usage hints if (banner) { if (user.bannerId) await DriveFiles.update(user.bannerId, { usageHint: null }); - await DriveFiles.update(banner.id, { usageHint: "user_banner" }); + await DriveFiles.update(banner.id, { usageHint: "userBanner" }); } if (Object.keys(updates).length > 0) await Users.update(user.id, updates); diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 2f975e1984..d180bbabf3 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -66,7 +66,7 @@ function urlPathJoin( * @param type Content-Type for original * @param hash Hash for original * @param size Size for original - * @param usage Optional usage hint for file (f.e. "user_avatar") + * @param usage Optional usage hint for file (f.e. "userAvatar") */ async function save( file: DriveFile, From ab221c98a74ee665784b773bb6ef02061a61946e Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Sat, 20 Apr 2024 00:05:37 +0800 Subject: [PATCH 093/110] revert unnecessary MaybeRef in components --- packages/client/src/components/MkChartTooltip.vue | 3 +-- .../client/src/components/MkReactionTooltip.vue | 3 +-- .../src/components/MkReactionsViewer.details.vue | 3 +-- packages/client/src/components/MkTooltip.vue | 13 +++---------- packages/client/src/components/MkUsersTooltip.vue | 3 +-- packages/client/src/components/MkWaitingDialog.vue | 9 ++++----- packages/client/src/os.ts | 9 +++++++-- 7 files changed, 18 insertions(+), 25 deletions(-) diff --git a/packages/client/src/components/MkChartTooltip.vue b/packages/client/src/components/MkChartTooltip.vue index 678a3ccdaa..cafc40413b 100644 --- a/packages/client/src/components/MkChartTooltip.vue +++ b/packages/client/src/components/MkChartTooltip.vue @@ -28,11 +28,10 @@ </template> <script lang="ts" setup> -import type { Ref } from "vue"; import MkTooltip from "./MkTooltip.vue"; const props = defineProps<{ - showing: Ref<boolean>; + showing: boolean; x: number; y: number; title?: string; diff --git a/packages/client/src/components/MkReactionTooltip.vue b/packages/client/src/components/MkReactionTooltip.vue index 68a1d6b625..40e8fefade 100644 --- a/packages/client/src/components/MkReactionTooltip.vue +++ b/packages/client/src/components/MkReactionTooltip.vue @@ -19,13 +19,12 @@ </template> <script lang="ts" setup> -import type { Ref } from "vue"; import type { entities } from "firefish-js"; import MkTooltip from "./MkTooltip.vue"; import XReactionIcon from "@/components/MkReactionIcon.vue"; defineProps<{ - showing: Ref<boolean>; + showing: boolean; reaction: string; emojis: entities.EmojiLite[]; targetElement: HTMLElement; diff --git a/packages/client/src/components/MkReactionsViewer.details.vue b/packages/client/src/components/MkReactionsViewer.details.vue index 7c5f118f98..13f8fd8311 100644 --- a/packages/client/src/components/MkReactionsViewer.details.vue +++ b/packages/client/src/components/MkReactionsViewer.details.vue @@ -30,13 +30,12 @@ </template> <script lang="ts" setup> -import type { Ref } from "vue"; import type { entities } from "firefish-js"; import MkTooltip from "./MkTooltip.vue"; import XReactionIcon from "@/components/MkReactionIcon.vue"; defineProps<{ - showing: Ref<boolean>; + showing: boolean; reaction: string; users: entities.User[]; // TODO count: number; diff --git a/packages/client/src/components/MkTooltip.vue b/packages/client/src/components/MkTooltip.vue index 2ed3c1974b..becc18fe09 100644 --- a/packages/client/src/components/MkTooltip.vue +++ b/packages/client/src/components/MkTooltip.vue @@ -5,7 +5,7 @@ @after-leave="emit('closed')" > <div - v-show="unref(showing)" + v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }" @@ -19,21 +19,14 @@ </template> <script lang="ts" setup> -import { - type MaybeRef, - nextTick, - onMounted, - onUnmounted, - ref, - unref, -} from "vue"; +import { nextTick, onMounted, onUnmounted, ref } from "vue"; import * as os from "@/os"; import { calcPopupPosition } from "@/scripts/popup-position"; import { defaultStore } from "@/store"; const props = withDefaults( defineProps<{ - showing: MaybeRef<boolean>; + showing: boolean; targetElement?: HTMLElement | null; x?: number; y?: number; diff --git a/packages/client/src/components/MkUsersTooltip.vue b/packages/client/src/components/MkUsersTooltip.vue index 30373c6eb3..30213204d6 100644 --- a/packages/client/src/components/MkUsersTooltip.vue +++ b/packages/client/src/components/MkUsersTooltip.vue @@ -19,12 +19,11 @@ </template> <script lang="ts" setup> -import type { Ref } from "vue"; import type { entities } from "firefish-js"; import MkTooltip from "./MkTooltip.vue"; defineProps<{ - showing: Ref<boolean>; + showing: boolean; users: entities.User[]; count: number; targetElement?: HTMLElement; diff --git a/packages/client/src/components/MkWaitingDialog.vue b/packages/client/src/components/MkWaitingDialog.vue index f2d7688f36..c35023adcb 100644 --- a/packages/client/src/components/MkWaitingDialog.vue +++ b/packages/client/src/components/MkWaitingDialog.vue @@ -13,7 +13,7 @@ ]" > <i - v-if="unref(success)" + v-if="success" :class="[$style.icon, $style.success, iconify('ph-check')]" ></i> <MkLoading @@ -29,16 +29,15 @@ </template> <script lang="ts" setup> -import type { MaybeRef } from "vue"; -import { shallowRef, unref, watch } from "vue"; +import { shallowRef, watch } from "vue"; import MkModal from "@/components/MkModal.vue"; import iconify from "@/scripts/icon"; const modal = shallowRef<InstanceType<typeof MkModal>>(); const props = defineProps<{ - success: MaybeRef<boolean>; - showing: MaybeRef<boolean>; + success: boolean; + showing: boolean; text?: string; }>(); diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index eb4502bf9f..c7c62852de 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -3,7 +3,7 @@ import { EventEmitter } from "eventemitter3"; import { type Endpoints, type entities, api as firefishApi } from "firefish-js"; import insertTextAtCursor from "insert-text-at-cursor"; -import type { Component, Ref } from "vue"; +import type { Component, MaybeRef, Ref } from "vue"; import { defineAsyncComponent, markRaw, ref } from "vue"; import { i18n } from "./i18n"; import MkDialog from "@/components/MkDialog.vue"; @@ -213,9 +213,13 @@ interface VueComponentConstructor<P, E> { type NonArrayAble<A> = A extends Array<unknown> ? never : A; +type CanUseRef<T> = { + [K in keyof T]: MaybeRef<T[K]>; +}; + export async function popup<Props, Emits>( component: VueComponentConstructor<Props, Emits>, - props: Props, + props: CanUseRef<Props>, events: Partial<NonArrayAble<NonNullable<Emits>>> = {}, disposeEvent?: keyof Partial<NonArrayAble<NonNullable<Emits>>>, ) { @@ -240,6 +244,7 @@ export async function popup<Props, Emits>( id, }; + // Hint: Vue will automatically resolve ref here, so it is safe to use ref in props popups.value.push(state); return { From 781c98dda77e2da84a4f9f7e37aeac8f477464d1 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Sat, 20 Apr 2024 00:18:36 +0800 Subject: [PATCH 094/110] revert unnecessary `.value` for MkLink --- packages/client/src/components/MkLink.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/components/MkLink.vue b/packages/client/src/components/MkLink.vue index 34db74397e..4add7c32da 100644 --- a/packages/client/src/components/MkLink.vue +++ b/packages/client/src/components/MkLink.vue @@ -42,7 +42,7 @@ useTooltip(el, (showing) => { os.popup( defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")), { - showing: showing.value, + showing, url: props.url, source: el.value, }, From 207855b0e8da93e681c75aa7909d12b78e2e2200 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Sat, 20 Apr 2024 01:03:22 +0800 Subject: [PATCH 095/110] fix: use settings from reactionPicker for non-reaction emoji picker --- packages/client/src/components/MkEmojiPicker.vue | 12 +++--------- .../client/src/components/MkEmojiPickerDialog.vue | 2 +- packages/client/src/components/MkPostForm.vue | 2 +- packages/client/src/scripts/reaction-picker.ts | 4 ++-- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue index afc2f15817..c731fe9b0e 100644 --- a/packages/client/src/components/MkEmojiPicker.vue +++ b/packages/client/src/components/MkEmojiPicker.vue @@ -231,15 +231,9 @@ const unicodeEmojiSkinToneLabels = [ i18n.ts._skinTones?.dark ?? "Dark", ]; -const size = computed(() => - props.asReactionPicker ? reactionPickerSize.value : 1, -); -const width = computed(() => - props.asReactionPicker ? reactionPickerWidth.value : 3, -); -const height = computed(() => - props.asReactionPicker ? reactionPickerHeight.value : 2, -); +const size = reactionPickerSize; +const width = reactionPickerWidth; +const height = reactionPickerHeight; const customEmojiCategories = emojiCategories; const customEmojis = instance.emojis; const q = ref<string | null>(null); diff --git a/packages/client/src/components/MkEmojiPickerDialog.vue b/packages/client/src/components/MkEmojiPickerDialog.vue index 426bbf9469..decf49fd38 100644 --- a/packages/client/src/components/MkEmojiPickerDialog.vue +++ b/packages/client/src/components/MkEmojiPickerDialog.vue @@ -39,7 +39,7 @@ import { defaultStore } from "@/store"; withDefaults( defineProps<{ manualShowing?: boolean | null; - src?: HTMLElement; + src?: HTMLElement | null; showPinned?: boolean; asReactionPicker?: boolean; }>(), diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue index 0a03dbb30e..582da6c6d4 100644 --- a/packages/client/src/components/MkPostForm.vue +++ b/packages/client/src/components/MkPostForm.vue @@ -1188,7 +1188,7 @@ async function insertEmoji(ev: MouseEvent) { os.openEmojiPicker( (ev.currentTarget ?? ev.target) as HTMLElement, {}, - textareaEl.value, + textareaEl.value!, ); } diff --git a/packages/client/src/scripts/reaction-picker.ts b/packages/client/src/scripts/reaction-picker.ts index 353a032d32..207d582343 100644 --- a/packages/client/src/scripts/reaction-picker.ts +++ b/packages/client/src/scripts/reaction-picker.ts @@ -24,14 +24,14 @@ class ReactionPicker { }, { done: (reaction) => { - this.onChosen!(reaction); + this.onChosen?.(reaction); }, close: () => { this.manualShowing.value = false; }, closed: () => { this.src.value = null; - this.onClosed!(); + this.onClosed?.(); }, }, ); From 5c4a773ecf1932ad5aefd50f50f824dcea1c9505 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sat, 20 Apr 2024 03:09:18 +0900 Subject: [PATCH 096/110] chore (backend): qualify Node.js builtin modules --- packages/backend/src/server/api/streaming.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts index a25984ec3e..12f97d8018 100644 --- a/packages/backend/src/server/api/streaming.ts +++ b/packages/backend/src/server/api/streaming.ts @@ -1,6 +1,6 @@ import type * as http from "node:http"; -import { EventEmitter } from "events"; -import type { ParsedUrlQuery } from "querystring"; +import { EventEmitter } from "node:events"; +import type { ParsedUrlQuery } from "node:querystring"; import * as websocket from "websocket"; import { subscriber as redisClient } from "@/db/redis.js"; From a2699e66871a237e124f03b00c8d8e89e75cdbce Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sat, 20 Apr 2024 23:04:12 +0900 Subject: [PATCH 097/110] chore (backend): fix imports --- packages/backend/src/misc/cache.ts | 2 +- packages/backend/src/server/activitypub.ts | 2 +- .../src/server/file/byte-range-readable.ts | 2 +- .../src/services/chart/charts/active-users.ts | 1 - packages/backend/src/services/fetch-rel-me.ts | 1 + packages/backend/src/services/note/read.ts | 31 ------------------- 6 files changed, 4 insertions(+), 35 deletions(-) diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 30a50e5714..e99b17a5f7 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,6 +1,6 @@ import { redisClient } from "@/db/redis.js"; import { encode, decode } from "msgpackr"; -import { ChainableCommander } from "ioredis"; +import type { ChainableCommander } from "ioredis"; export class Cache<T> { private ttl: number; diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 00c8a6babe..71d95709b7 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -32,7 +32,7 @@ import Followers from "./activitypub/followers.js"; import Outbox, { packActivity } from "./activitypub/outbox.js"; import { serverLogger } from "./index.js"; import config from "@/config/index.js"; -import Koa from "koa"; +import type Koa from "koa"; import * as crypto from "node:crypto"; import { inspect } from "node:util"; import type { IActivity } from "@/remote/activitypub/type.js"; diff --git a/packages/backend/src/server/file/byte-range-readable.ts b/packages/backend/src/server/file/byte-range-readable.ts index 96dcbc4a52..9699f95092 100644 --- a/packages/backend/src/server/file/byte-range-readable.ts +++ b/packages/backend/src/server/file/byte-range-readable.ts @@ -1,4 +1,4 @@ -import { Readable, ReadableOptions } from "node:stream"; +import { Readable, type ReadableOptions } from "node:stream"; import { Buffer } from "node:buffer"; import * as fs from "node:fs"; diff --git a/packages/backend/src/services/chart/charts/active-users.ts b/packages/backend/src/services/chart/charts/active-users.ts index 3f4b7e3381..067334005e 100644 --- a/packages/backend/src/services/chart/charts/active-users.ts +++ b/packages/backend/src/services/chart/charts/active-users.ts @@ -1,4 +1,3 @@ -import type { KVs } from "../core.js"; import Chart from "../core.js"; import type { User } from "@/models/entities/user.js"; import { name, schema } from "./entities/active-users.js"; diff --git a/packages/backend/src/services/fetch-rel-me.ts b/packages/backend/src/services/fetch-rel-me.ts index c9a37d1c88..70faa01aa7 100644 --- a/packages/backend/src/services/fetch-rel-me.ts +++ b/packages/backend/src/services/fetch-rel-me.ts @@ -1,4 +1,5 @@ import { Window } from "happy-dom"; +import type { HTMLAnchorElement, HTMLLinkElement } from "happy-dom"; import config from "@/config/index.js"; async function getRelMeLinks(url: string): Promise<string[]> { diff --git a/packages/backend/src/services/note/read.ts b/packages/backend/src/services/note/read.ts index 3c49501416..7eab66c41b 100644 --- a/packages/backend/src/services/note/read.ts +++ b/packages/backend/src/services/note/read.ts @@ -3,7 +3,6 @@ import type { Note } from "@/models/entities/note.js"; import type { User } from "@/models/entities/user.js"; import { NoteUnreads, - Users, Followings, ChannelFollowings, } from "@/models/index.js"; @@ -120,34 +119,4 @@ export default async function ( ]), }); } - - // if (readAntennaNotes.length > 0) { - // await AntennaNotes.update( - // { - // antennaId: In(myAntennas.map((a) => a.id)), - // noteId: In(readAntennaNotes.map((n) => n.id)), - // }, - // { - // read: true, - // }, - // ); - - // // TODO: まとめてクエリしたい - // for (const antenna of myAntennas) { - // const count = await AntennaNotes.countBy({ - // antennaId: antenna.id, - // read: false, - // }); - - // if (count === 0) { - // publishMainStream(userId, "readAntenna", antenna); - // } - // } - - // Users.getHasUnreadAntenna(userId).then((unread) => { - // if (!unread) { - // publishMainStream(userId, "readAllAntennas"); - // } - // }); - // } } From 488323cc8ec252447907a4b7e9f19a7b7ef7c24e Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 21 Apr 2024 05:57:05 +0900 Subject: [PATCH 098/110] chore: format --- packages/backend/src/services/note/read.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/backend/src/services/note/read.ts b/packages/backend/src/services/note/read.ts index 7eab66c41b..d7fda27a85 100644 --- a/packages/backend/src/services/note/read.ts +++ b/packages/backend/src/services/note/read.ts @@ -1,11 +1,7 @@ import { publishMainStream } from "@/services/stream.js"; import type { Note } from "@/models/entities/note.js"; import type { User } from "@/models/entities/user.js"; -import { - NoteUnreads, - Followings, - ChannelFollowings, -} from "@/models/index.js"; +import { NoteUnreads, Followings, ChannelFollowings } from "@/models/index.js"; import { Not, IsNull, In } from "typeorm"; import type { Channel } from "@/models/entities/channel.js"; import { readNotificationByQuery } from "@/server/api/common/read-notification.js"; From 2760e7feeeba3d68a8770d3458adf3a53a22dfae Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 21 Apr 2024 06:40:53 +0900 Subject: [PATCH 099/110] chore (minor): use ** in lieu of Math.pow --- packages/backend/src/queue/initialize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/queue/initialize.ts b/packages/backend/src/queue/initialize.ts index 0f9c83132f..a874005fbd 100644 --- a/packages/backend/src/queue/initialize.ts +++ b/packages/backend/src/queue/initialize.ts @@ -34,7 +34,7 @@ export function initialize<T>(name: string, limitPerSec = -1) { function apBackoff(attemptsMade: number, err: Error) { const baseDelay = 60 * 1000; // 1min const maxBackoff = 8 * 60 * 60 * 1000; // 8hours - let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay; + let backoff = (2 ** attemptsMade - 1) * baseDelay; backoff = Math.min(backoff, maxBackoff); backoff += Math.round(backoff * Math.random() * 0.2); return backoff; From dc02a077742bfc81bba089acd69520f189d64fda Mon Sep 17 00:00:00 2001 From: mei23 <m@m544.net> Date: Sun, 21 Apr 2024 09:29:00 +0900 Subject: [PATCH 100/110] fix (backend): add Cache-Control to Bull Dashboard --- packages/backend/src/server/web/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 939fcfab14..4473165be7 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -54,6 +54,10 @@ app.use(async (ctx, next) => { const url = decodeURI(ctx.path); if (url === bullBoardPath || url.startsWith(`${bullBoardPath}/`)) { + if (!url.startsWith(`${bullBoardPath}/static/`)) { + ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); + } + const token = ctx.cookies.get("token"); if (token == null) { ctx.status = 401; From d1e898c0d0eb1d9258bffed10816cce510847077 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 21 Apr 2024 09:32:05 +0900 Subject: [PATCH 101/110] docs: update changelog --- docs/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index a818e09835..860dfbad8c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,10 @@ Critical security updates are indicated by the :warning: icon. - Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well. - Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well. +## Unreleased + +- Fix bugs + ## [v20240413](https://firefish.dev/firefish/firefish/-/merge_requests/10741/commits) - Add "Media" tab to user page From dac4043dd9bdca046c4a60b1a1662abdfff76dde Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 21 Apr 2024 10:09:45 +0900 Subject: [PATCH 102/110] v20240421 --- docs/changelog.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 860dfbad8c..70f8b34fbe 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,7 +5,7 @@ Critical security updates are indicated by the :warning: icon. - Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well. - Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well. -## Unreleased +## [v20240421](https://firefish.dev/firefish/firefish/-/merge_requests/10756/commits) - Fix bugs diff --git a/package.json b/package.json index dd0766bc7b..a62fae09f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firefish", - "version": "20240413", + "version": "20240421", "repository": { "type": "git", "url": "https://firefish.dev/firefish/firefish.git" From 9f3396af21bf459394287cdf16656a72065a8b5b Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 21 Apr 2024 10:30:13 +0900 Subject: [PATCH 103/110] chore (backend): translate Japanese comments into English --- packages/backend/src/services/logger.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts index e53279e31c..3fcf28671e 100644 --- a/packages/backend/src/services/logger.ts +++ b/packages/backend/src/services/logger.ts @@ -144,12 +144,12 @@ export default class Logger { } } + // Used when the process can't continue (fatal error) public error( x: string | Error, data?: Record<string, any> | null, important = false, ): void { - // 実行を継続できない状況で使う if (x instanceof Error) { data = data || {}; data.e = x; @@ -166,30 +166,30 @@ export default class Logger { } } + // Used when the process can continue but some action should be taken public warn( message: string, data?: Record<string, any> | null, important = false, ): void { - // 実行を継続できるが改善すべき状況で使う this.log("warning", message, data, important); } + // Used when something is successful public succ( message: string, data?: Record<string, any> | null, important = false, ): void { - // 何かに成功した状況で使う this.log("success", message, data, important); } + // Used for debugging (information necessary for developers but unnecessary for users) public debug( message: string, data?: Record<string, any> | null, important = false, ): void { - // Used for debugging (information necessary for developers but unnecessary for users) // Fixed if statement is ignored when logLevel includes debug if ( config.logLevel?.includes("debug") || @@ -200,12 +200,12 @@ export default class Logger { } } + // Other generic logs public info( message: string, data?: Record<string, any> | null, important = false, ): void { - // それ以外 this.log("info", message, data, important); } } From 28f7ac1acd835ea2059289b060ec6b74b7609929 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 21 Apr 2024 10:31:00 +0900 Subject: [PATCH 104/110] fix (backend): typo --- packages/backend/src/services/logger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts index 3fcf28671e..f4b4454ef8 100644 --- a/packages/backend/src/services/logger.ts +++ b/packages/backend/src/services/logger.ts @@ -28,9 +28,9 @@ export default class Logger { if (config.syslog) { this.syslogClient = new SyslogPro.RFC5424({ - applacationName: "Firefish", + applicationName: "Firefish", timestamp: true, - encludeStructuredData: true, + includeStructuredData: true, color: true, extendedColor: true, server: { From 43570a54aa3e146698487d5ee39686f428b91397 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 21 Apr 2024 10:44:54 +0900 Subject: [PATCH 105/110] chore: format --- packages/backend/src/services/drive/upload-from-url.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts index a96e8e3262..e7b084bda1 100644 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ b/packages/backend/src/services/drive/upload-from-url.ts @@ -3,7 +3,10 @@ import type { User } from "@/models/entities/user.js"; import { createTemp } from "@/misc/create-temp.js"; import { downloadUrl, isPrivateIp } from "@/misc/download-url.js"; import type { DriveFolder } from "@/models/entities/drive-folder.js"; -import type { DriveFile, DriveFileUsageHint } from "@/models/entities/drive-file.js"; +import type { + DriveFile, + DriveFileUsageHint, +} from "@/models/entities/drive-file.js"; import { DriveFiles } from "@/models/index.js"; import { driveLogger } from "./logger.js"; import { addFile } from "./add-file.js"; From c936102a4c95b4b6e12b3caf46ef9bcdde4f4e2c Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 21 Apr 2024 10:45:47 +0900 Subject: [PATCH 106/110] chore (backend-rs): regenerate entities and index.js/d.ts --- packages/backend-rs/index.d.ts | 5 +++++ packages/backend-rs/index.js | 3 ++- packages/backend-rs/src/model/entity/drive_file.rs | 3 ++- .../src/model/entity/sea_orm_active_enums.rs | 14 ++++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 1cf961bd30..5a7bf218c5 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -348,6 +348,7 @@ export interface DriveFile { webpublicType: string | null requestHeaders: Json | null requestIp: string | null + usageHint: DriveFileUsageHintEnum | null } export interface DriveFolder { id: string @@ -780,6 +781,10 @@ export enum AntennaSrcEnum { List = 'list', Users = 'users' } +export enum DriveFileUsageHintEnum { + UserAvatar = 'userAvatar', + UserBanner = 'userBanner' +} export enum MutedNoteReasonEnum { Manual = 'manual', Other = 'other', diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 1ea7bb5bed..6d64f6ec75 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding module.exports.readEnvironmentConfig = readEnvironmentConfig module.exports.readServerConfig = readServerConfig @@ -339,6 +339,7 @@ module.exports.decodeReaction = decodeReaction module.exports.countReactions = countReactions module.exports.toDbReaction = toDbReaction module.exports.AntennaSrcEnum = AntennaSrcEnum +module.exports.DriveFileUsageHintEnum = DriveFileUsageHintEnum module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum module.exports.NoteVisibilityEnum = NoteVisibilityEnum module.exports.NotificationTypeEnum = NotificationTypeEnum diff --git a/packages/backend-rs/src/model/entity/drive_file.rs b/packages/backend-rs/src/model/entity/drive_file.rs index e5c3995573..a6926e7af2 100644 --- a/packages/backend-rs/src/model/entity/drive_file.rs +++ b/packages/backend-rs/src/model/entity/drive_file.rs @@ -1,5 +1,6 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 +use super::sea_orm_active_enums::DriveFileUsageHintEnum; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -53,7 +54,7 @@ pub struct Model { #[sea_orm(column_name = "requestIp")] pub request_ip: Option<String>, #[sea_orm(column_name = "usageHint")] - pub usage_hint: Option<String>, + pub usage_hint: Option<DriveFileUsageHintEnum>, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs index 38820e1bd8..36281f4dc5 100644 --- a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs +++ b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs @@ -23,6 +23,20 @@ pub enum AntennaSrcEnum { #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] #[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "drive_file_usage_hint_enum" +)] +pub enum DriveFileUsageHintEnum { + #[sea_orm(string_value = "userAvatar")] + UserAvatar, + #[sea_orm(string_value = "userBanner")] + UserBanner, +} +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[cfg_attr(not(feature = "napi"), derive(Clone))] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", From 96481f1353dd665b0c05d776d5df54f6ce2a57a8 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 21 Apr 2024 10:48:31 +0900 Subject: [PATCH 107/110] chore: update downgrade.sql --- docs/downgrade.sql | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/downgrade.sql b/docs/downgrade.sql index 77dea27573..44222f818f 100644 --- a/docs/downgrade.sql +++ b/docs/downgrade.sql @@ -1,6 +1,7 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'AddDriveFileUsage1713451569342', 'ConvertCwVarcharToText1713225866247', 'FixChatFileConstraint1712855579316', 'DropTimeZone1712425488543', @@ -23,7 +24,11 @@ DELETE FROM "migrations" WHERE name IN ( 'RemoveNativeUtilsMigration1705877093218' ); ---convert-cw-varchar-to-text +-- AddDriveFileUsage +ALTER TABLE "drive_file" DROP COLUMN "usageHint"; +DROP TYPE "drive_file_usage_hint_enum"; + +-- convert-cw-varchar-to-text DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"; ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512); CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2); From d2dbfb37c72234e4b4a3b23867c48ec9034861b3 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 21 Apr 2024 10:59:02 +0900 Subject: [PATCH 108/110] chore (backend): reflect entity changes to the schema and repository --- packages/backend/src/models/repositories/drive-file.ts | 2 ++ packages/backend/src/models/schema/drive-file.ts | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index 2321f20d4c..18b139caff 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -152,6 +152,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ md5: file.md5, size: file.size, isSensitive: file.isSensitive, + usageHint: file.usageHint, blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file, false), @@ -193,6 +194,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ md5: file.md5, size: file.size, isSensitive: file.isSensitive, + usageHint: file.usageHint, blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file, false), diff --git a/packages/backend/src/models/schema/drive-file.ts b/packages/backend/src/models/schema/drive-file.ts index 30db9e7d48..929dbb472e 100644 --- a/packages/backend/src/models/schema/drive-file.ts +++ b/packages/backend/src/models/schema/drive-file.ts @@ -44,6 +44,12 @@ export const packedDriveFileSchema = { optional: false, nullable: false, }, + usageHint: { + type: "string", + optional: false, + nullable: true, + enum: ["userAvatar", "userBanner"], + }, blurhash: { type: "string", optional: false, From 6b008c651a899a47b202f1d25b6c5ed56577e962 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 21 Apr 2024 11:09:18 +0900 Subject: [PATCH 109/110] chore (backend): remove (technically) incorrect TypeORM decorator field --- packages/backend/src/models/entities/drive-file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 2c6c1bf598..81f564115f 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -179,11 +179,11 @@ export class DriveFile { }) public isSensitive: boolean; + // Hint for what this file is used for @Column({ type: "enum", enum: ["userAvatar", "userBanner"], nullable: true, - comment: "Hint for what the file is used for.", }) public usageHint: DriveFileUsageHint; From ce672f4edd542d78a1b33429d379ec969ae163cb Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Sun, 21 Apr 2024 22:36:05 +0900 Subject: [PATCH 110/110] dev: add cargo test to pnpm scripts mocha test has been unmaintained for a long time and is very broken :( --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a62fae09f0..93594a5698 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "debug": "pnpm run build:debug && pnpm run start", "build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp", "mocha": "pnpm --filter backend run mocha", - "test": "pnpm run mocha", + "test": "pnpm run test:ts && pnpm run test:rs", + "test:ts": "pnpm run mocha", + "test:rs": "cargo test", "format": "pnpm run format:ts; pnpm run format:rs", "format:ts": "pnpm -r --parallel run format", "format:rs": "cargo fmt --all --",