diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d934f6758a..2e973b55b5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -98,6 +98,7 @@ test:build:backend_ts_only: changes: paths: - packages/backend/**/* + - packages/firefish-js/**/* - packages/megalodon/**/* when: always before_script: @@ -115,7 +116,7 @@ test:build:backend_ts_only: - psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga' script: - pnpm install --frozen-lockfile - - pnpm --filter 'backend' --filter 'megalodon' run build:debug + - pnpm --filter 'backend' --filter 'firefish-js' --filter 'megalodon' run build:debug - pnpm run migrate test:build:client_only: diff --git a/packages/backend/package.json b/packages/backend/package.json index c427084f05..ddc28deca1 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -55,6 +55,7 @@ "escape-regexp": "0.0.1", "feed": "4.2.2", "file-type": "19.0.0", + "firefish-js": "workspace:*", "fluent-ffmpeg": "2.1.2", "form-data": "4.0.0", "got": "14.2.1", diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index 73600832ce..b4148e7e27 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -1,234 +1,7 @@ -import { - packedUserLiteSchema, - packedUserDetailedNotMeOnlySchema, - packedMeDetailedOnlySchema, - packedUserDetailedNotMeSchema, - packedMeDetailedSchema, - packedUserDetailedSchema, - packedUserSchema, -} from "@/models/schema/user.js"; -import { packedNoteSchema } from "@/models/schema/note.js"; -import { packedUserListSchema } from "@/models/schema/user-list.js"; -import { packedAppSchema } from "@/models/schema/app.js"; -import { packedMessagingMessageSchema } from "@/models/schema/messaging-message.js"; -import { packedNotificationSchema } from "@/models/schema/notification.js"; -import { packedDriveFileSchema } from "@/models/schema/drive-file.js"; -import { packedDriveFolderSchema } from "@/models/schema/drive-folder.js"; -import { packedFollowingSchema } from "@/models/schema/following.js"; -import { packedMutingSchema } from "@/models/schema/muting.js"; -import { packedRenoteMutingSchema } from "@/models/schema/renote-muting.js"; -import { packedReplyMutingSchema } from "@/models/schema/reply-muting.js"; -import { packedBlockingSchema } from "@/models/schema/blocking.js"; -import { packedNoteReactionSchema } from "@/models/schema/note-reaction.js"; -import { packedHashtagSchema } from "@/models/schema/hashtag.js"; -import { packedPageSchema } from "@/models/schema/page.js"; -import { packedUserGroupSchema } from "@/models/schema/user-group.js"; -import { packedNoteFavoriteSchema } from "@/models/schema/note-favorite.js"; -import { packedChannelSchema } from "@/models/schema/channel.js"; -import { packedAntennaSchema } from "@/models/schema/antenna.js"; -import { packedClipSchema } from "@/models/schema/clip.js"; -import { packedFederationInstanceSchema } from "@/models/schema/federation-instance.js"; -import { packedQueueCountSchema } from "@/models/schema/queue.js"; -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"; +// TODO: use firefish-js +import { Schema as _Schema } from "firefish-js"; -export const refs = { - AbuseUserReport: packedAbuseUserReportSchema, - UserLite: packedUserLiteSchema, - UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema, - MeDetailedOnly: packedMeDetailedOnlySchema, - UserDetailedNotMe: packedUserDetailedNotMeSchema, - MeDetailed: packedMeDetailedSchema, - UserDetailed: packedUserDetailedSchema, - User: packedUserSchema, - - UserList: packedUserListSchema, - UserGroup: packedUserGroupSchema, - App: packedAppSchema, - MessagingMessage: packedMessagingMessageSchema, - Note: packedNoteSchema, - NoteFile: packedNoteFileSchema, - NoteEdit: packedNoteEdit, - NoteReaction: packedNoteReactionSchema, - NoteFavorite: packedNoteFavoriteSchema, - Notification: packedNotificationSchema, - DriveFile: packedDriveFileSchema, - DriveFolder: packedDriveFolderSchema, - Following: packedFollowingSchema, - Muting: packedMutingSchema, - RenoteMuting: packedRenoteMutingSchema, - ReplyMuting: packedReplyMutingSchema, - Blocking: packedBlockingSchema, - Hashtag: packedHashtagSchema, - Page: packedPageSchema, - Channel: packedChannelSchema, - QueueCount: packedQueueCountSchema, - Antenna: packedAntennaSchema, - Clip: packedClipSchema, - FederationInstance: packedFederationInstanceSchema, - GalleryPost: packedGalleryPostSchema, - Emoji: packedEmojiSchema, -}; - -export type Packed<x extends keyof typeof refs> = SchemaType<(typeof refs)[x]>; - -type TypeStringef = - | "null" - | "boolean" - | "integer" - | "number" - | "string" - | "array" - | "object" - | "any"; -type StringDefToType<T extends TypeStringef> = T extends "null" - ? null - : T extends "boolean" - ? boolean - : T extends "integer" - ? number - : T extends "number" - ? number - : T extends "string" - ? string | Date - : T extends "array" - ? ReadonlyArray<any> - : T extends "object" - ? Record<string, any> - : any; - -// https://swagger.io/specification/?sbsearch=optional#schema-object -type OfSchema = { - readonly anyOf?: ReadonlyArray<Schema>; - readonly oneOf?: ReadonlyArray<Schema>; - readonly allOf?: ReadonlyArray<Schema>; -}; - -export interface Schema extends OfSchema { - readonly type?: TypeStringef; - readonly nullable?: boolean; - readonly optional?: boolean; - readonly items?: Schema; - readonly properties?: Obj; - readonly required?: ReadonlyArray< - Extract<keyof NonNullable<this["properties"]>, string> - >; - readonly description?: string; - readonly example?: any; - readonly format?: string; - readonly ref?: keyof typeof refs; - readonly enum?: ReadonlyArray<string>; - readonly default?: - | (this["type"] extends TypeStringef ? StringDefToType<this["type"]> : any) - | null; - readonly maxLength?: number; - readonly minLength?: number; - readonly maximum?: number; - readonly minimum?: number; - readonly pattern?: string; -} - -type RequiredPropertyNames<s extends Obj> = { - [K in keyof s]: // K is not optional - s[K]["optional"] extends false - ? K - : // K has default value - s[K]["default"] extends - | null - | string - | number - | boolean - | Record<string, unknown> - ? K - : never; -}[keyof s]; - -export type Obj = Record<string, Schema>; - -// https://github.com/misskey-dev/misskey/issues/8535 -// To avoid excessive stack depth error, -// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it). -export type ObjType< - s extends Obj, - RequiredProps extends keyof s, -> = UnionToIntersection< - { - -readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]>; - } & { - -readonly [R in RequiredProps]-?: SchemaType<s[R]>; - } & { - -readonly [P in keyof s]?: SchemaType<s[P]>; - } ->; - -type NullOrUndefined<p extends Schema, T> = - | (p["nullable"] extends true ? null : never) - | (p["optional"] extends true ? undefined : never) - | T; - -// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection -// Get intersection from union -type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ( - k: infer I, -) => void - ? I - : never; - -// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 -// To get union, we use `Foo extends any ? Hoge<Foo> : never` -type UnionSchemaType< - a extends readonly any[], - X extends Schema = a[number], -> = X extends any ? SchemaType<X> : never; -type ArrayUnion<T> = T extends any ? Array<T> : never; - -export type SchemaTypeDef<p extends Schema> = p["type"] extends "null" - ? null - : p["type"] extends "integer" - ? number - : p["type"] extends "number" - ? number - : p["type"] extends "string" - ? p["enum"] extends readonly string[] - ? p["enum"][number] - : p["format"] extends "date-time" - ? string - : // Dateにする?? - string - : p["type"] extends "boolean" - ? boolean - : p["type"] extends "object" - ? p["ref"] extends keyof typeof refs - ? Packed<p["ref"]> - : p["properties"] extends NonNullable<Obj> - ? ObjType<p["properties"], NonNullable<p["required"]>[number]> - : p["anyOf"] extends ReadonlyArray<Schema> - ? UnionSchemaType<p["anyOf"]> & - Partial<UnionToIntersection<UnionSchemaType<p["anyOf"]>>> - : p["allOf"] extends ReadonlyArray<Schema> - ? UnionToIntersection<UnionSchemaType<p["allOf"]>> - : any - : p["type"] extends "array" - ? p["items"] extends OfSchema - ? p["items"]["anyOf"] extends ReadonlyArray<Schema> - ? UnionSchemaType<NonNullable<p["items"]["anyOf"]>>[] - : p["items"]["oneOf"] extends ReadonlyArray<Schema> - ? ArrayUnion< - UnionSchemaType<NonNullable<p["items"]["oneOf"]>> - > - : p["items"]["allOf"] extends ReadonlyArray<Schema> - ? UnionToIntersection< - UnionSchemaType<NonNullable<p["items"]["allOf"]>> - >[] - : never - : p["items"] extends NonNullable<Schema> - ? SchemaTypeDef<p["items"]>[] - : any[] - : p["oneOf"] extends ReadonlyArray<Schema> - ? UnionSchemaType<p["oneOf"]> - : any; - -export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>; +export const refs = _Schema.refs; +export type Packed<T extends keyof typeof refs> = _Schema.Packed<T>; +export type Schema = _Schema.Schema; +export type SchemaType<P extends _Schema.Schema> = _Schema.SchemaType<P>; diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 04684ebc10..65ad217d51 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -53,7 +53,7 @@ import { UserProfiles } from "@/models/index.js"; import { In } from "typeorm"; import { config } from "@/config.js"; import { truncate } from "@/misc/truncate.js"; -import { langmap } from "@/misc/langmap.js"; +import { langmap } from "firefish-js"; import { inspect } from "node:util"; export function validateNote(object: any, uri: string) { diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index eb4d9ca5a2..7f07bb2336 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -17,7 +17,7 @@ import { ApiError } from "@/server/api/error.js"; import define from "@/server/api/define.js"; import { HOUR } from "backend-rs"; import { getNote } from "@/server/api/common/getters.js"; -import { langmap } from "@/misc/langmap.js"; +import { langmap } from "firefish-js"; export const meta = { tags: ["notes"], diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 8e92d43bf4..4698d3d473 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -33,7 +33,7 @@ import renderNote from "@/remote/activitypub/renderer/note.js"; import renderUpdate from "@/remote/activitypub/renderer/update.js"; import { deliverToRelays } from "@/services/relay.js"; // import { deliverQuestionUpdate } from "@/services/note/polls/update.js"; -import { langmap } from "@/misc/langmap.js"; +import { langmap } from "firefish-js"; export const meta = { tags: ["notes"], diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 094b7fed33..8b84baa0b2 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -1,7 +1,7 @@ import { ApiError } from "@/server/api/error.js"; import { getNote } from "@/server/api/common/getters.js"; import { translate } from "@/misc/translate.js"; -import type { PostLanguage } from "@/misc/langmap.js"; +import type { PostLanguage } from "firefish-js"; import define from "@/server/api/define.js"; export const meta = { diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 2096f8b1a2..0e484ffc3c 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -63,7 +63,7 @@ import { db } from "@/db/postgre.js"; import { getActiveWebhooks } from "@/misc/webhook-cache.js"; import { redisClient } from "@/db/redis.js"; import { Mutex } from "redis-semaphore"; -import { langmap } from "@/misc/langmap.js"; +import { langmap } from "firefish-js"; import Logger from "@/services/logger.js"; import { inspect } from "node:util"; import { toRustObject } from "@/prelude/undefined-to-null.js"; diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index fb3f70c34e..1d38b2798f 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -7,6 +7,7 @@ import { alert, api, popup, popupMenu, waiting } from "@/os"; import icon from "@/scripts/icon"; import { del, get, set } from "@/scripts/idb-proxy"; import { reloadChannel, unisonReload } from "@/scripts/unison-reload"; +import type { MenuButton, MenuUser } from "./types/menu"; // TODO: 他のタブと永続化されたstateを同期 @@ -16,7 +17,7 @@ export async function signOut() { waiting(); localStorage.removeItem("account"); - await removeAccount(me.id); + await removeAccount(me!.id); const accounts = await getAccounts(); @@ -26,12 +27,9 @@ export async function signOut() { const registration = await navigator.serviceWorker.ready; const push = await registration.pushManager.getSubscription(); if (push) { - await fetch(`${apiUrl}/sw/unregister`, { - method: "POST", - body: JSON.stringify({ - i: me.token, - endpoint: push.endpoint, - }), + await api("sw/unregister", { + endpoint: push.endpoint, + i: me!.token, // FIXME: This parameter seems to be removable but I didn't test it }); } } @@ -117,13 +115,13 @@ function showSuspendedDialog() { export function updateAccount(accountData) { for (const [key, value] of Object.entries(accountData)) { - me[key] = value; + me![key] = value; } localStorage.setItem("account", JSON.stringify(me)); } export async function refreshAccount() { - const accountData = await fetchAccount(me.token); + const accountData = await fetchAccount(me!.token); return updateAccount(accountData); } @@ -189,7 +187,7 @@ export async function openAccountMenu( async function switchAccount(account: entities.UserDetailed) { const storedAccounts = await getAccounts(); - const token = storedAccounts.find((x) => x.id === account.id).token; + const token = storedAccounts.find((x) => x.id === account.id)!.token; switchAccountWithToken(token); } @@ -198,15 +196,15 @@ export async function openAccountMenu( } const storedAccounts = await getAccounts().then((accounts) => - accounts.filter((x) => x.id !== me.id), + accounts.filter((x) => x.id !== me!.id), ); const accountsPromise = api("users/show", { userIds: storedAccounts.map((x) => x.id), }); - function createItem(account: entities.UserDetailed) { + function createItem(account: entities.UserDetailed): MenuUser { return { - type: "user", + type: "user" as const, user: account, active: opts.active != null ? opts.active === account.id : false, action: () => { @@ -221,10 +219,14 @@ export async function openAccountMenu( const accountItemPromises = storedAccounts.map( (a) => - new Promise((res) => { + new Promise<MenuUser>((res) => { accountsPromise.then((accounts) => { const account = accounts.find((x) => x.id === a.id); - if (account == null) return res(null); + if (account == null) { + // The user is deleted, remove it + removeAccount(a.id); + return res(null as unknown as MenuUser); + } res(createItem(account)); }); }), @@ -233,74 +235,72 @@ export async function openAccountMenu( if (opts.withExtraOperation) { popupMenu( [ - ...[ - ...(isMobile ?? false - ? [ - { - type: "parent", - icon: `${icon("ph-plus")}`, - text: i18n.ts.addAccount, - children: [ - { - text: i18n.ts.existingAccount, - action: () => { - showSigninDialog(); - }, + ...(isMobile ?? false + ? [ + { + type: "parent" as const, + icon: `${icon("ph-plus")}`, + text: i18n.ts.addAccount, + children: [ + { + text: i18n.ts.existingAccount, + action: () => { + showSigninDialog(); }, - { - text: i18n.ts.createAccount, - action: () => { - createAccount(); - }, + }, + { + text: i18n.ts.createAccount, + action: () => { + createAccount(); }, - ], - }, - ] - : [ - { - type: "link", - text: i18n.ts.profile, - to: `/@${me.username}`, - avatar: me, - }, - null, - ]), - ...(opts.includeCurrentAccount ? [createItem(me)] : []), - ...accountItemPromises, - ...(isMobile ?? false - ? [ - null, - { - type: "link", - text: i18n.ts.profile, - to: `/@${me.username}`, - avatar: me, - }, - ] - : [ - { - type: "parent", - icon: `${icon("ph-plus")}`, - text: i18n.ts.addAccount, - children: [ - { - text: i18n.ts.existingAccount, - action: () => { - showSigninDialog(); - }, + }, + ], + }, + ] + : [ + { + type: "link" as const, + text: i18n.ts.profile, + to: `/@${me!.username}`, + avatar: me!, + }, + null, + ]), + ...(opts.includeCurrentAccount ? [createItem(me!)] : []), + ...accountItemPromises, + ...(isMobile ?? false + ? [ + null, + { + type: "link" as const, + text: i18n.ts.profile, + to: `/@${me!.username}`, + avatar: me!, + }, + ] + : [ + { + type: "parent" as const, + icon: `${icon("ph-plus")}`, + text: i18n.ts.addAccount, + children: [ + { + text: i18n.ts.existingAccount, + action: () => { + showSigninDialog(); }, - { - text: i18n.ts.createAccount, - action: () => { - createAccount(); - }, + }, + { + text: i18n.ts.createAccount, + action: () => { + createAccount(); }, - ], - }, - ]), - ], + }, + ], + }, + ]), ], - ev.currentTarget ?? ev.target, + (ev.currentTarget ?? ev.target) as HTMLElement, { align: "left", }, @@ -308,10 +308,10 @@ export async function openAccountMenu( } else { popupMenu( [ - ...(opts.includeCurrentAccount ? [createItem(me)] : []), + ...(opts.includeCurrentAccount ? [createItem(me!)] : []), ...accountItemPromises, ], - ev.currentTarget ?? ev.target, + (ev.currentTarget ?? ev.target) as HTMLElement, { align: "left", }, diff --git a/packages/client/src/cold-store.ts b/packages/client/src/cold-store.ts new file mode 100644 index 0000000000..63e7782264 --- /dev/null +++ b/packages/client/src/cold-store.ts @@ -0,0 +1,121 @@ +import { ref as vueRef } from "vue"; +import type { UnwrapRef } from "vue"; + +// TODO: 他のタブと永続化されたstateを同期 + +const PREFIX = "miux:"; + +interface Plugin { + id: string; + name: string; + active: boolean; + configData: Record<string, unknown>; + token: string; + ast: unknown[]; +} + +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"; + +const ColdStoreDefault = { + lightTheme, + darkTheme, + syncDeviceDarkMode: true, + plugins: [] as Plugin[], + mediaVolume: 0.5, + vibrate: false, + sound_masterVolume: 0.3, + sound_note: { type: "none", volume: 0 }, + sound_noteMy: { type: "syuilo/up", volume: 1 }, + sound_notification: { type: "syuilo/pope2", volume: 1 }, + sound_chat: { type: "syuilo/pope1", volume: 1 }, + sound_chatBg: { type: "syuilo/waon", volume: 1 }, + sound_antenna: { type: "syuilo/triple", volume: 1 }, + sound_channel: { type: "syuilo/square-pico", volume: 1 }, +}; + +const watchers: { + key: string; + callback: (value) => void; +}[] = []; + +function get<T extends keyof typeof ColdStoreDefault>( + key: T, +): (typeof ColdStoreDefault)[T] { + // TODO: indexedDBにする + // ただしその際はnullチェックではなくキー存在チェックにしないとダメ + // (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある) + const value = localStorage.getItem(PREFIX + key); + if (value == null) { + return ColdStoreDefault[key]; + } else { + return JSON.parse(value); + } +} + +function set<T extends keyof typeof ColdStoreDefault>( + key: T, + value: (typeof ColdStoreDefault)[T], +): void { + // 呼び出し側のバグ等で undefined が来ることがある + // undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 + if (value === undefined) { + console.error(`attempt to store undefined value for key '${key}'`); + return; + } + + localStorage.setItem(PREFIX + key, JSON.stringify(value)); + + for (const watcher of watchers) { + if (watcher.key === key) watcher.callback(value); + } +} + +function watch<T extends keyof typeof ColdStoreDefault>( + key: T, + callback: (value: (typeof ColdStoreDefault)[T]) => void, +) { + watchers.push({ key, callback }); +} + +// TODO: VueのcustomRef使うと良い感じになるかも +function ref<T extends keyof typeof ColdStoreDefault>(key: T) { + const v = get(key); + const r = vueRef(v); + // TODO: このままではwatcherがリークするので開放する方法を考える + watch(key, (v) => { + r.value = v as UnwrapRef<typeof v>; + }); + return r; +} + +/** + * 特定のキーの、簡易的なgetter/setterを作ります + * 主にvue場で設定コントロールのmodelとして使う用 + */ +function makeGetterSetter<K extends keyof typeof ColdStoreDefault>(key: K) { + // TODO: VueのcustomRef使うと良い感じになるかも + const valueRef = ref(key); + return { + get: () => { + return valueRef.value; + }, + set: (value: (typeof ColdStoreDefault)[K]) => { + const val = value; + set(key, val); + }, + }; +} + +export default { + default: ColdStoreDefault, + watchers, + get, + set, + watch, + ref, + makeGetterSetter, +}; diff --git a/packages/client/src/components/MkAnnouncement.vue b/packages/client/src/components/MkAnnouncement.vue index 3af8e0163f..783d511bc8 100644 --- a/packages/client/src/components/MkAnnouncement.vue +++ b/packages/client/src/components/MkAnnouncement.vue @@ -1,5 +1,5 @@ <template> - <MkModal ref="modal" :z-priority="'middle'" @closed="$emit('closed')"> + <MkModal ref="modal" :z-priority="'middle'" @closed="emit('closed')"> <div :class="$style.root"> <div :class="$style.title"> <MkSparkle v-if="isGoodNews">{{ title }}</MkSparkle> @@ -41,6 +41,10 @@ const props = defineProps<{ announcement: entities.Announcement; }>(); +const emit = defineEmits<{ + closed: []; +}>(); + const { id, text, title, imageUrl, isGoodNews } = props.announcement; const modal = shallowRef<InstanceType<typeof MkModal>>(); diff --git a/packages/client/src/components/MkAutocomplete.vue b/packages/client/src/components/MkAutocomplete.vue index 332785b467..4e890cc03e 100644 --- a/packages/client/src/components/MkAutocomplete.vue +++ b/packages/client/src/components/MkAutocomplete.vue @@ -182,7 +182,7 @@ export default { const props = defineProps<{ type: string; q: string | null; - textarea: HTMLTextAreaElement; + textarea: HTMLTextAreaElement | HTMLInputElement; close: () => void; x: number; y: number; @@ -435,7 +435,7 @@ onUpdated(() => { onMounted(() => { setPosition(); - props.textarea.addEventListener("keydown", onKeydown); + (props.textarea as HTMLTextAreaElement).addEventListener("keydown", onKeydown); document.body.addEventListener("mousedown", onMousedown); nextTick(() => { @@ -453,7 +453,7 @@ onMounted(() => { }); onBeforeUnmount(() => { - props.textarea.removeEventListener("keydown", onKeydown); + (props.textarea as HTMLTextAreaElement).removeEventListener("keydown", onKeydown); document.body.removeEventListener("mousedown", onMousedown); }); </script> diff --git a/packages/client/src/components/MkManyAnnouncements.vue b/packages/client/src/components/MkManyAnnouncements.vue index 903891b64c..047c92787c 100644 --- a/packages/client/src/components/MkManyAnnouncements.vue +++ b/packages/client/src/components/MkManyAnnouncements.vue @@ -1,5 +1,5 @@ <template> - <MkModal ref="modal" :z-priority="'middle'" @closed="$emit('closed')"> + <MkModal ref="modal" :z-priority="'middle'" @closed="emit('closed')"> <div :class="$style.root"> <p :class="$style.title"> {{ i18n.ts.youHaveUnreadAnnouncements }} @@ -21,6 +21,10 @@ import MkModal from "@/components/MkModal.vue"; import MkButton from "@/components/MkButton.vue"; import { i18n } from "@/i18n"; +const emit = defineEmits<{ + closed: []; +}>(); + const modal = shallowRef<InstanceType<typeof MkModal>>(); const checkAnnouncements = () => { modal.value!.close(); diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue index 78cb6350f7..702627f8b7 100644 --- a/packages/client/src/components/MkPostForm.vue +++ b/packages/client/src/components/MkPostForm.vue @@ -810,13 +810,17 @@ function setLanguage() { actions.push(null); } - if (language.value != null) + if (language.value != null && langmap[language.value] != null) { actions.push({ text: langmap[language.value].nativeName, danger: false, active: true, action: () => {}, }); + } else { + // Unrecognized language, set to null + language.value = null; + } const langs = Object.keys(langmap); diff --git a/packages/client/src/components/MkSignin.vue b/packages/client/src/components/MkSignin.vue index d2c1159642..35770a2d13 100644 --- a/packages/client/src/components/MkSignin.vue +++ b/packages/client/src/components/MkSignin.vue @@ -160,7 +160,7 @@ const hCaptchaResponse = ref(null); const reCaptchaResponse = ref(null); const emit = defineEmits<{ - (ev: "login", v: any): void; + login: [v: { id: string; i: string }]; }>(); const props = defineProps({ diff --git a/packages/client/src/components/MkSigninDialog.vue b/packages/client/src/components/MkSigninDialog.vue index b0fc7700e8..f63e7c2565 100644 --- a/packages/client/src/components/MkSigninDialog.vue +++ b/packages/client/src/components/MkSigninDialog.vue @@ -30,7 +30,7 @@ withDefaults( ); const emit = defineEmits<{ - (ev: "done"): void; + (ev: "done", res: { id: string; i: string }): void; (ev: "closed"): void; (ev: "cancelled"): void; }>(); @@ -39,11 +39,11 @@ const dialog = ref<InstanceType<typeof XModalWindow>>(); function onClose() { emit("cancelled"); - dialog.value.close(); + dialog.value!.close(); } -function onLogin(res) { +function onLogin(res: { id: string; i: string }) { emit("done", res); - dialog.value.close(); + dialog.value!.close(); } </script> diff --git a/packages/client/src/components/MkSignup.vue b/packages/client/src/components/MkSignup.vue index 0807935be3..05119809d4 100644 --- a/packages/client/src/components/MkSignup.vue +++ b/packages/client/src/components/MkSignup.vue @@ -248,7 +248,7 @@ v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" - :sitekey="instance.hcaptchaSiteKey" + :sitekey="instance.hcaptchaSiteKey!" /> <MkCaptcha v-if="instance.enableRecaptcha" @@ -256,7 +256,7 @@ v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" - :sitekey="instance.recaptchaSiteKey" + :sitekey="instance.recaptchaSiteKey!" /> <MkButton class="_formBlock" @@ -296,7 +296,7 @@ const props = withDefaults( ); const emit = defineEmits<{ - (ev: "signup", user: Record<string, any>): void; + (ev: "signup", user: { id: string; i: string }): void; (ev: "signupEmailPending"): void; }>(); diff --git a/packages/client/src/components/MkSignupDialog.vue b/packages/client/src/components/MkSignupDialog.vue index f9829c1040..5614188198 100644 --- a/packages/client/src/components/MkSignupDialog.vue +++ b/packages/client/src/components/MkSignupDialog.vue @@ -36,13 +36,13 @@ withDefaults( ); const emit = defineEmits<{ - (ev: "done"): void; + (ev: "done", res: { id: string; i: string }): void; (ev: "closed"): void; }>(); const dialog = ref<InstanceType<typeof XModalWindow>>(); -function onSignup(res) { +function onSignup(res: { id: string; i: string }) { emit("done", res); dialog.value?.close(); } diff --git a/packages/client/src/components/MkUpdated.vue b/packages/client/src/components/MkUpdated.vue index 7f519b407a..5061e9d91a 100644 --- a/packages/client/src/components/MkUpdated.vue +++ b/packages/client/src/components/MkUpdated.vue @@ -2,8 +2,8 @@ <MkModal ref="modal" :z-priority="'middle'" - @click="$refs.modal.close()" - @closed="$emit('closed')" + @click="modal!.close()" + @closed="emit('closed')" > <div :class="$style.root"> <div :class="$style.title"> @@ -14,7 +14,7 @@ :class="$style.gotIt" primary full - @click="$refs.modal.close()" + @click="modal!.close()" >{{ i18n.ts.gotIt }}</MkButton > </div> diff --git a/packages/client/src/i18n.ts b/packages/client/src/i18n.ts index 6d352ba03e..4dd989a64a 100644 --- a/packages/client/src/i18n.ts +++ b/packages/client/src/i18n.ts @@ -1,6 +1,7 @@ import { markRaw } from "vue"; import { locale } from "@/config"; +// biome-ignore lint/suspicious/noExplicitAny: temporary use any class I18n<T extends Record<string, any>> { public ts: T; diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts index 08d424cffc..c3fff47e9a 100644 --- a/packages/client/src/init.ts +++ b/packages/client/src/init.ts @@ -245,7 +245,12 @@ function checkForSplash() { try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため - if (lastVersion < version && defaultStore.state.showUpdates) { + // If a strange version string comes, an error will occur in compareVersions. + if ( + lastVersion != null && + lastVersion < version && + defaultStore.state.showUpdates + ) { // ログインしてる場合だけ if (me) { popup( @@ -281,7 +286,7 @@ function checkForSplash() { "closed", ); } else { - unreadAnnouncements.forEach((item) => { + for (const item of unreadAnnouncements) { if (item.showPopup) popup( defineAsyncComponent( @@ -291,7 +296,7 @@ function checkForSplash() { {}, "closed", ); - }); + } } }) .catch((err) => console.log(err)); diff --git a/packages/client/src/nirax.ts b/packages/client/src/nirax.ts index da162338b6..90348b06ba 100644 --- a/packages/client/src/nirax.ts +++ b/packages/client/src/nirax.ts @@ -1,7 +1,7 @@ // NIRAX --- A lightweight router import { EventEmitter } from "eventemitter3"; -import type { Component, ShallowRef } from "vue"; +import type { Component } from "vue"; import { shallowRef } from "vue"; import { safeURIDecode } from "@/scripts/safe-uri-decode"; import { pleaseLogin } from "@/scripts/please-login"; @@ -36,6 +36,7 @@ export interface Resolved { function parsePath(path: string): ParsedPath { const res = [] as ParsedPath; + // biome-ignore lint/style/noParameterAssign: assign it intentionally path = path.substring(1); for (const part of path.split("/")) { @@ -76,13 +77,13 @@ export class Router extends EventEmitter<{ same: () => void; }> { private routes: RouteDef[]; - public current: Resolved; - public currentRef: ShallowRef<Resolved> = shallowRef(); - public currentRoute: ShallowRef<RouteDef> = shallowRef(); + public current!: Resolved; // It is assigned in this.navigate + public currentRef = shallowRef<Resolved>(); + public currentRoute = shallowRef<RouteDef>(); private currentPath: string; private currentKey = Date.now().toString(); - public navHook: ((path: string, flag?: any) => boolean) | null = null; + public navHook: ((path: string, flag?: unknown) => boolean) | null = null; constructor(routes: Router["routes"], currentPath: Router["currentPath"]) { super(); @@ -92,9 +93,10 @@ export class Router extends EventEmitter<{ this.navigate(currentPath, null, false); } - public resolve(path: string): Resolved | null { + public resolve(_path: string): Resolved | null { let queryString: string | null = null; let hash: string | null = null; + let path = _path; if (path[0] === "/") path = path.substring(1); if (path.includes("#")) { hash = path.substring(path.indexOf("#") + 1); @@ -168,9 +170,16 @@ export class Router extends EventEmitter<{ } if (route.query != null && queryString != null) { - const queryObject = [ - ...new URLSearchParams(queryString).entries(), - ].reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {}); + // const queryObject = [ + // ...new URLSearchParams(queryString).entries(), + // ].reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {}); + + const queryObject: Record<string, string> = Object.assign( + {}, + ...[...new URLSearchParams(queryString).entries()].map( + (entry) => ({ [entry[0]]: entry[1] }), + ), + ); for (const q in route.query) { const as = route.query[q]; @@ -227,6 +236,7 @@ export class Router extends EventEmitter<{ } const isSamePath = beforePath === path; + // biome-ignore lint/style/noParameterAssign: assign it intentionally if (isSamePath && key == null) key = this.currentKey; this.current = res; this.currentRef.value = res; @@ -253,7 +263,7 @@ export class Router extends EventEmitter<{ return this.currentKey; } - public push(path: string, flag?: any) { + public push(path: string, flag?: unknown) { const beforePath = this.currentPath; if (path === beforePath) { this.emit("same"); diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 4ccd5e47b1..b997da28ac 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -896,9 +896,6 @@ export async function openEmojiPicker( ...opts, }, { - chosen: (emoji) => { - insertTextAtCursor(activeTextarea, emoji); - }, done: (emoji) => { insertTextAtCursor(activeTextarea, emoji); }, diff --git a/packages/client/src/pages/settings/delete-account.vue b/packages/client/src/pages/settings/delete-account.vue index 9c24dd4009..48b0b3448e 100644 --- a/packages/client/src/pages/settings/delete-account.vue +++ b/packages/client/src/pages/settings/delete-account.vue @@ -7,7 +7,7 @@ i18n.ts._accountDelete.sendEmail }}</FormInfo> <FormButton - v-if="!me?.isDeleted" + v-if="!me!.isDeleted" danger class="_formBlock" @click="deleteAccount" diff --git a/packages/client/src/pizzax.ts b/packages/client/src/pizzax.ts index ce35dbeb24..0185071f30 100644 --- a/packages/client/src/pizzax.ts +++ b/packages/client/src/pizzax.ts @@ -5,12 +5,13 @@ import { onUnmounted, ref, watch } from "vue"; import { api } from "./os"; import { useStream } from "./stream"; import { isSignedIn, me } from "@/me"; +import type { TypeUtils } from "firefish-js"; type StateDef = Record< string, { where: "account" | "device" | "deviceAccount"; - default: any; + default: unknown; } >; @@ -82,11 +83,12 @@ export class Storage<T extends StateDef> { for (const [k, v] of Object.entries(state)) { reactiveState[k] = ref(v); } - this.state = state as any; - this.reactiveState = reactiveState as any; + this.state = state as typeof this.state; + this.reactiveState = reactiveState as typeof this.reactiveState; if (isSignedIn(me)) { // なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう) + // For some reason, if I don't setTimeout, an error occurs in the api function (probably caused by circular references) window.setTimeout(() => { api("i/registry/get-all", { scope: ["client", this.key] }).then( (kvs) => { @@ -104,7 +106,7 @@ export class Storage<T extends StateDef> { } } localStorage.setItem( - `${this.keyForLocalStorage}::cache::${me.id}`, + `${this.keyForLocalStorage}::cache::${me!.id}`, JSON.stringify(cache), ); }, @@ -118,11 +120,12 @@ export class Storage<T extends StateDef> { key, value, }: { - scope: string[]; + scope?: string[]; key: keyof T; value: T[typeof key]["default"]; }) => { if ( + scope == null || scope.length !== 2 || scope[0] !== "client" || scope[1] !== this.key || @@ -135,13 +138,13 @@ export class Storage<T extends StateDef> { const cache = JSON.parse( localStorage.getItem( - `${this.keyForLocalStorage}::cache::${me.id}`, + `${this.keyForLocalStorage}::cache::${me!.id}`, ) || "{}", ); if (cache[key] !== value) { cache[key] = value; localStorage.setItem( - `${this.keyForLocalStorage}::cache::${me.id}`, + `${this.keyForLocalStorage}::cache::${me!.id}`, JSON.stringify(cache), ); } @@ -150,7 +153,7 @@ export class Storage<T extends StateDef> { } } - public set<K extends keyof T>(key: K, value: T[K]["default"]): void { + public set<K extends keyof T>(key: K & string, value: T[K]["default"]): void { if (_DEV_) console.log("set", key, value); this.state[key] = value; @@ -201,15 +204,15 @@ export class Storage<T extends StateDef> { } } - public push<K extends keyof T>( - key: K, + public push<K extends TypeUtils.PropertyOfType<T, { default: unknown[] }>>( + key: K & string, value: ArrayElement<T[K]["default"]>, ): void { - const currentState = this.state[key]; + const currentState = this.state[key] as unknown[]; this.set(key, [...currentState, value]); } - public reset(key: keyof T) { + public reset(key: keyof T & string) { this.set(key, this.def[key].default); } @@ -218,11 +221,11 @@ export class Storage<T extends StateDef> { * 主にvue場で設定コントロールのmodelとして使う用 */ public makeGetterSetter<K extends keyof T>( - key: K, - getter?: (v: T[K]) => unknown, - setter?: (v: unknown) => T[K], + key: K & string, + getter?: (oldV: T[K]["default"]) => T[K]["default"], + setter?: (oldV: T[K]["default"]) => T[K]["default"], ) { - const valueRef = ref(this.state[key]); + const valueRef = ref(this.state[key]) as Ref<T[K]["default"]>; const stop = watch(this.reactiveState[key], (val) => { valueRef.value = val; @@ -242,7 +245,7 @@ export class Storage<T extends StateDef> { return valueRef.value; } }, - set: (value: unknown) => { + set: (value: T[K]["default"]) => { const val = setter ? setter(value) : value; this.set(key, val); valueRef.value = val; diff --git a/packages/client/src/plugin.ts b/packages/client/src/plugin.ts index c38aaa365a..000071b7d8 100644 --- a/packages/client/src/plugin.ts +++ b/packages/client/src/plugin.ts @@ -42,7 +42,20 @@ export function install(plugin) { aiscript.exec(parser.parse(plugin.src)); } -function createPluginEnv(opts) { +interface Plugin { + config?: Record< + string, + { + default: unknown; + [k: string]: unknown; + } + >; + configData: Record<string, unknown>; + token: string; + id: string; +} + +function createPluginEnv(opts: { plugin: Plugin; storageKey: string }) { const config = new Map<string, values.Value>(); for (const [k, v] of Object.entries(opts.plugin.config ?? {})) { config.set( @@ -172,7 +185,7 @@ function registerNoteAction({ pluginId, title, handler }) { if (!pluginContext) { return; } - pluginContext.execFn(handler, [utils.jsToVal(user)]); + pluginContext.execFn(handler, [utils.jsToVal(note)]); }, }); } @@ -205,16 +218,18 @@ function registerNotePostInterruptor({ pluginId, handler }) { }); } +// FIXME: where is pageViewInterruptors? +// This function currently can't do anything function registerPageViewInterruptor({ pluginId, handler }): void { - pageViewInterruptors.push({ - handler: async (page) => { - const pluginContext = pluginContexts.get(pluginId); - if (!pluginContext) { - return; - } - return utils.valToJs( - await pluginContext.execFn(handler, [utils.jsToVal(page)]), - ); - }, - }); + // pageViewInterruptors.push({ + // handler: async (page) => { + // const pluginContext = pluginContexts.get(pluginId); + // if (!pluginContext) { + // return; + // } + // return utils.valToJs( + // await pluginContext.execFn(handler, [utils.jsToVal(page)]), + // ); + // }, + // }); } diff --git a/packages/client/src/scripts/2fa.ts b/packages/client/src/scripts/2fa.ts index 9c34c8fb70..f5d1f57538 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) => Number.parseInt(byte, 16)), + string.match(/.{1,2}/g)!.map((byte) => Number.parseInt(byte, 16)), ); } } diff --git a/packages/client/src/scripts/array.ts b/packages/client/src/scripts/array.ts index ef3dfb298e..f9c7525638 100644 --- a/packages/client/src/scripts/array.ts +++ b/packages/client/src/scripts/array.ts @@ -1,4 +1,4 @@ -import type { EndoRelation, Predicate } from "./relation"; +import type { EndoRelation, Predicate } from "@/types/relation"; /** * Count the number of elements that satisfy the predicate @@ -126,7 +126,7 @@ export function lessThan(xs: number[], ys: number[]): boolean { * Returns the longest prefix of elements that satisfy the predicate */ export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] { - const ys = []; + const ys: T[] = []; for (const x of xs) { if (f(x)) { ys.push(x); diff --git a/packages/client/src/scripts/autocomplete.ts b/packages/client/src/scripts/autocomplete.ts index 18e56ba38d..9eedb33d4d 100644 --- a/packages/client/src/scripts/autocomplete.ts +++ b/packages/client/src/scripts/autocomplete.ts @@ -13,7 +13,7 @@ export class Autocomplete { } | null; private textarea: HTMLInputElement | HTMLTextAreaElement; - private currentType: string; + private currentType?: string; private textRef: Ref<string>; private opening: boolean; @@ -69,7 +69,7 @@ export class Autocomplete { * テキスト入力時 */ private onInput() { - const caretPos = this.textarea.selectionStart; + const caretPos = this.textarea.selectionStart!; const text = this.text.substring(0, caretPos).split("\n").pop()!; const mentionIndex = text.lastIndexOf("@"); @@ -147,10 +147,10 @@ export class Autocomplete { this.opening = true; this.currentType = type; - // #region サジェストを表示すべき位置を計算 + // #region Calculate the position where suggestions should be displayed const caretPosition = getCaretCoordinates( this.textarea, - this.textarea.selectionStart, + this.textarea.selectionStart!, ); const rect = this.textarea.getBoundingClientRect(); @@ -216,7 +216,7 @@ export class Autocomplete { private complete({ type, value }) { this.close(); - const caret = this.textarea.selectionStart; + const caret = this.textarea.selectionStart!; if (type === "user") { const source = this.text; diff --git a/packages/client/src/scripts/check-word-mute.ts b/packages/client/src/scripts/check-word-mute.ts index b8e17c4a6f..1a45be2571 100644 --- a/packages/client/src/scripts/check-word-mute.ts +++ b/packages/client/src/scripts/check-word-mute.ts @@ -1,4 +1,5 @@ import type { entities } from "firefish-js"; +import { detectLanguage, languageContains } from "./language-utils"; export interface Muted { muted: boolean; @@ -12,11 +13,12 @@ function checkLangMute( note: entities.Note, mutedLangs: Array<string | string[]>, ): Muted { - const mutedLangList = new Set( - mutedLangs.reduce((arr, x) => [...arr, ...(Array.isArray(x) ? x : [x])]), - ); - if (mutedLangList.has((note.lang?.[0]?.lang || "").split("-")[0])) { - return { muted: true, matched: [note.lang?.[0]?.lang] }; + const mutedLangList = mutedLangs.flat(); + const noteLang = note.lang ?? detectLanguage(note.text ?? "") ?? "no-lang"; + for (const mutedLang of mutedLangList) { + if (languageContains(mutedLang, noteLang)) { + return { muted: true, matched: [noteLang] }; + } } return NotMuted; } @@ -32,7 +34,7 @@ function checkWordMute( if (text === "") return NotMuted; - const result = { muted: false, matched: [] }; + const result = { muted: false, matched: [] as string[] }; for (const mutePattern of mutedWords) { if (Array.isArray(mutePattern)) { @@ -74,7 +76,7 @@ function checkWordMute( } export function getWordSoftMute( - note: firefish.entities.Note, + note: entities.Note, meId: string | null | undefined, mutedWords: Array<string | string[]>, mutedLangs: Array<string | string[]>, diff --git a/packages/client/src/scripts/collect-page-vars.ts b/packages/client/src/scripts/collect-page-vars.ts index e7d4a6d0e6..d410981e9a 100644 --- a/packages/client/src/scripts/collect-page-vars.ts +++ b/packages/client/src/scripts/collect-page-vars.ts @@ -1,6 +1,8 @@ -export function collectPageVars(content) { - const pageVars = []; - const collect = (xs: any[]) => { +import type { PageContent, PageVar } from "@/types/page"; + +export function collectPageVars(content: PageContent[]) { + const pageVars: PageVar[] = []; + const collect = (xs: PageContent[]) => { for (const x of xs) { if (x.type === "textInput") { pageVars.push({ @@ -24,7 +26,7 @@ export function collectPageVars(content) { pageVars.push({ name: x.name, type: "boolean", - value: x.default, + value: x.default!, }); } else if (x.type === "counter") { pageVars.push({ diff --git a/packages/client/src/scripts/copy-to-clipboard.ts b/packages/client/src/scripts/copy-to-clipboard.ts index a4835d8e79..b989a1bc43 100644 --- a/packages/client/src/scripts/copy-to-clipboard.ts +++ b/packages/client/src/scripts/copy-to-clipboard.ts @@ -1,7 +1,7 @@ /** * Clipboardに値をコピー(TODO: 文字列以外も対応) */ -export default (val) => { +function obsoleteCopyToClipboard(val: string) { // 空div 生成 const tmp = document.createElement("div"); // 選択用のタグ生成 @@ -21,7 +21,7 @@ export default (val) => { // body に追加 document.body.appendChild(tmp); // 要素を選択 - document.getSelection().selectAllChildren(tmp); + document.getSelection()?.selectAllChildren(tmp); // クリップボードにコピー const result = document.execCommand("copy"); @@ -30,4 +30,20 @@ export default (val) => { document.body.removeChild(tmp); return result; -}; +} + +export default async function (val?: string | null) { + if (val == null) return true; + const clipboardObj = window.navigator?.clipboard; + if (clipboardObj == null) { + // not supported + return obsoleteCopyToClipboard(val); + } else { + return new Promise<boolean>((res) => { + clipboardObj + .writeText(val) + .then(() => res(true)) + .catch(() => res(obsoleteCopyToClipboard(val))); + }); + } +} diff --git a/packages/client/src/scripts/extract-mentions.ts b/packages/client/src/scripts/extract-mentions.ts index 259f78e576..cdf04c1106 100644 --- a/packages/client/src/scripts/extract-mentions.ts +++ b/packages/client/src/scripts/extract-mentions.ts @@ -6,7 +6,10 @@ export function extractMentions( nodes: mfm.MfmNode[], ): mfm.MfmMention["props"][] { // TODO: 重複を削除 - const mentionNodes = mfm.extract(nodes, (node) => node.type === "mention"); + const mentionNodes = mfm.extract( + nodes, + (node) => node.type === "mention", + ) as mfm.MfmMention[]; const mentions = mentionNodes.map((x) => x.props); return mentions; diff --git a/packages/client/src/scripts/extract-mfm.ts b/packages/client/src/scripts/extract-mfm.ts index b02557b341..e5dfb7029b 100644 --- a/packages/client/src/scripts/extract-mfm.ts +++ b/packages/client/src/scripts/extract-mfm.ts @@ -15,7 +15,8 @@ const animatedMfm = [ export function extractMfmWithAnimation(nodes: mfm.MfmNode[]): string[] { const mfmNodes = mfm.extract(nodes, (node) => { return node.type === "fn" && animatedMfm.includes(node.props.name); - }); + }) as mfm.MfmFn[]; + // FIXME: mfm type error const mfms = mfmNodes.map((x) => x.props.fn); return mfms; diff --git a/packages/client/src/scripts/extract-url-from-mfm.ts b/packages/client/src/scripts/extract-url-from-mfm.ts index 0c580b6d32..e142640df3 100644 --- a/packages/client/src/scripts/extract-url-from-mfm.ts +++ b/packages/client/src/scripts/extract-url-from-mfm.ts @@ -14,7 +14,7 @@ export function extractUrlFromMfm( node.type === "url" || (node.type === "link" && !(respectSilentFlag && node.props.silent)) ); - }); + }) as (mfm.MfmLink | mfm.MfmUrl)[]; const urls: string[] = unique(urlNodes.map((x) => x.props.url)); return urls.reduce((array, url) => { diff --git a/packages/client/src/scripts/focus.ts b/packages/client/src/scripts/focus.ts index 878132fbe8..d490a1045d 100644 --- a/packages/client/src/scripts/focus.ts +++ b/packages/client/src/scripts/focus.ts @@ -1,5 +1,6 @@ export function focusPrev(el: Element | null, self = false, scroll = true) { if (el == null) return; + // biome-ignore lint/style/noParameterAssign: assign it intentionally if (!self) el = el.previousElementSibling; if (el) { if (el.hasAttribute("tabindex")) { @@ -14,6 +15,7 @@ export function focusPrev(el: Element | null, self = false, scroll = true) { export function focusNext(el: Element | null, self = false, scroll = true) { if (el == null) return; + // biome-ignore lint/style/noParameterAssign: assign it intentionally if (!self) el = el.nextElementSibling; if (el) { if (el.hasAttribute("tabindex")) { diff --git a/packages/client/src/scripts/gen-search-query.ts b/packages/client/src/scripts/gen-search-query.ts deleted file mode 100644 index 21bc5df34e..0000000000 --- a/packages/client/src/scripts/gen-search-query.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { acct } from "firefish-js"; -import { host as localHost } from "@/config"; - -export async function genSearchQuery(v: any, q: string) { - let host: string; - let userId: string; - if (q.split(" ").some((x) => x.startsWith("@"))) { - for (const at of q - .split(" ") - .filter((x) => x.startsWith("@")) - .map((x) => x.slice(1))) { - if (at.includes(".")) { - if (at === localHost || at === ".") { - host = null; - } else { - host = at; - } - } else { - const user = await v.os - .api("users/show", acct.parse(at)) - .catch((x) => null); - if (user) { - userId = user.id; - } else { - // todo: show error - } - } - } - } - return { - query: q - .split(" ") - .filter((x) => !(x.startsWith("/") || x.startsWith("@"))) - .join(" "), - host, - userId, - }; -} diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 2945cb16b3..ade43a3e22 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -15,6 +15,7 @@ import { useRouter } from "@/router"; import { notePage } from "@/filters/note"; import type { NoteTranslation } from "@/types/note"; import type { MenuItem } from "@/types/menu"; +import type { NoteDraft } from "@/types/post-form"; const router = useRouter(); @@ -72,7 +73,7 @@ export function getNoteMenu(props: { }); os.post({ - initialNote: appearNote, + initialNote: appearNote as NoteDraft, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel, @@ -83,7 +84,7 @@ export function getNoteMenu(props: { async function edit() { os.post({ - initialNote: appearNote, + initialNote: appearNote as NoteDraft, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel, diff --git a/packages/client/src/scripts/langmap.ts b/packages/client/src/scripts/langmap.ts index bfb6bec00d..ae82146bec 100644 --- a/packages/client/src/scripts/langmap.ts +++ b/packages/client/src/scripts/langmap.ts @@ -1,385 +1,6 @@ -// TODO: sharedに置いてバックエンドのと統合したい -export const iso639Langs1 = { - af: { - nativeName: "Afrikaans", - }, - ak: { - nativeName: "Tɕɥi", - }, - ar: { - nativeName: "العربية", - rtl: true, - }, - ay: { - nativeName: "Aymar aru", - }, - az: { - nativeName: "Azərbaycan dili", - }, - be: { - nativeName: "Беларуская", - }, - bg: { - nativeName: "Български", - }, - bn: { - nativeName: "বাংলা", - }, - br: { - nativeName: "Brezhoneg", - }, - bs: { - nativeName: "Bosanski", - }, - ca: { - nativeName: "Català", - }, - cs: { - nativeName: "Čeština", - }, - cy: { - nativeName: "Cymraeg", - }, - da: { - nativeName: "Dansk", - }, - de: { - nativeName: "Deutsch", - }, - el: { - nativeName: "Ελληνικά", - }, - en: { - nativeName: "English", - }, - eo: { - nativeName: "Esperanto", - }, - es: { - nativeName: "Español", - }, - et: { - nativeName: "eesti keel", - }, - eu: { - nativeName: "Euskara", - }, - fa: { - nativeName: "فارسی", - rtl: true, - }, - ff: { - nativeName: "Fulah", - }, - fi: { - nativeName: "Suomi", - }, - fo: { - nativeName: "Føroyskt", - }, - fr: { - nativeName: "Français", - }, - fy: { - nativeName: "Frysk", - }, - ga: { - nativeName: "Gaeilge", - }, - gd: { - nativeName: "Gàidhlig", - }, - gl: { - nativeName: "Galego", - }, - gn: { - nativeName: "Avañe'ẽ", - }, - gu: { - nativeName: "ગુજરાતી", - }, - gv: { - nativeName: "Gaelg", - }, - he: { - nativeName: "עברית", - rtl: true, - }, - hi: { - nativeName: "हिन्दी", - }, - hr: { - nativeName: "Hrvatski", - }, - ht: { - nativeName: "Kreyòl", - }, - hu: { - nativeName: "Magyar", - }, - hy: { - nativeName: "Հայերեն", - }, - id: { - nativeName: "Bahasa Indonesia", - }, - is: { - nativeName: "Íslenska", - }, - it: { - nativeName: "Italiano", - }, - ja: { - nativeName: "日本語", - }, - jv: { - nativeName: "Basa Jawa", - }, - ka: { - nativeName: "ქართული", - }, - kk: { - nativeName: "Қазақша", - }, - kl: { - nativeName: "kalaallisut", - }, - km: { - nativeName: "ភាសាខ្មែរ", - }, - kn: { - nativeName: "ಕನ್ನಡ", - }, - ko: { - nativeName: "한국어", - }, - ku: { - nativeName: "Kurdî", - }, - kw: { - nativeName: "Kernewek", - }, - la: { - nativeName: "Latin", - }, - lb: { - nativeName: "Lëtzebuergesch", - }, - li: { - nativeName: "Lèmbörgs", - }, - lt: { - nativeName: "Lietuvių", - }, - lv: { - nativeName: "Latviešu", - }, - mg: { - nativeName: "Malagasy", - }, - mk: { - nativeName: "Македонски", - }, - ml: { - nativeName: "മലയാളം", - }, - mn: { - nativeName: "Монгол", - }, - mr: { - nativeName: "मराठी", - }, - ms: { - nativeName: "Bahasa Melayu", - }, - mt: { - nativeName: "Malti", - }, - my: { - nativeName: "ဗမာစကာ", - }, - no: { - nativeName: "Norsk", - }, - nb: { - nativeName: "Norsk (bokmål)", - }, - ne: { - nativeName: "नेपाली", - }, - nl: { - nativeName: "Nederlands", - }, - nn: { - nativeName: "Norsk (nynorsk)", - }, - oc: { - nativeName: "Occitan", - }, - or: { - nativeName: "ଓଡ଼ିଆ", - }, - pa: { - nativeName: "ਪੰਜਾਬੀ", - }, - pl: { - nativeName: "Polski", - }, - ps: { - nativeName: "پښتو", - rtl: true, - }, - pt: { - nativeName: "Português", - }, - qu: { - nativeName: "Qhichwa", - }, - rm: { - nativeName: "Rumantsch", - }, - ro: { - nativeName: "Română", - }, - ru: { - nativeName: "Русский", - }, - sa: { - nativeName: "संस्कृतम्", - }, - se: { - nativeName: "Davvisámegiella", - }, - sh: { - nativeName: "српскохрватски", - }, - si: { - nativeName: "සිංහල", - }, - sk: { - nativeName: "Slovenčina", - }, - sl: { - nativeName: "Slovenščina", - }, - so: { - nativeName: "Soomaaliga", - }, - sq: { - nativeName: "Shqip", - }, - sr: { - nativeName: "Српски", - }, - su: { - nativeName: "Basa Sunda", - }, - sv: { - nativeName: "Svenska", - }, - sw: { - nativeName: "Kiswahili", - }, - ta: { - nativeName: "தமிழ்", - }, - te: { - nativeName: "తెలుగు", - }, - tg: { - nativeName: "забо́ни тоҷикӣ́", - }, - th: { - nativeName: "ภาษาไทย", - }, - tr: { - nativeName: "Türkçe", - }, - tt: { - nativeName: "татарча", - }, - uk: { - nativeName: "Українська", - }, - ur: { - nativeName: "اردو", - rtl: true, - }, - uz: { - nativeName: "O'zbek", - }, - vi: { - nativeName: "Tiếng Việt", - }, - xh: { - nativeName: "isiXhosa", - }, - yi: { - nativeName: "ייִדיש", - rtl: true, - }, - zh: { - nativeName: "中文", - }, - zu: { - nativeName: "isiZulu", - }, -}; +import { langmap as _langmap } from "firefish-js"; -export const iso639Langs3 = { - ach: { - nativeName: "Lwo", - }, - ady: { - nativeName: "Адыгэбзэ", - }, - cak: { - nativeName: "Maya Kaqchikel", - }, - chr: { - nativeName: "ᏣᎳᎩ (tsalagi)", - }, - dsb: { - nativeName: "Dolnoserbšćina", - }, - fil: { - nativeName: "Filipino", - }, - hsb: { - nativeName: "Hornjoserbšćina", - }, - kab: { - nativeName: "Taqbaylit", - }, - mai: { - nativeName: "मैथिली, মৈথিলী", - }, - tlh: { - nativeName: "tlhIngan-Hol", - }, - tok: { - nativeName: "Toki Pona", - }, - yue: { - nativeName: "粵語", - }, - nan: { - nativeName: "閩南語", - }, -}; - -export const langmapNoRegion = Object.assign({}, iso639Langs1, iso639Langs3); - -export const iso639Regional = { - "zh-hans": { - nativeName: "中文(简体)", - }, - "zh-hant": { - nativeName: "中文(繁體)", - }, -}; - -export const langmap = Object.assign({}, langmapNoRegion, iso639Regional); +export const langmap = _langmap; /** * @see https://github.com/komodojp/tinyld/blob/develop/docs/langs.md diff --git a/packages/client/src/scripts/language-utils.ts b/packages/client/src/scripts/language-utils.ts index 93696028de..b73348ba8f 100644 --- a/packages/client/src/scripts/language-utils.ts +++ b/packages/client/src/scripts/language-utils.ts @@ -39,7 +39,7 @@ export function languageContains( ) { if (!langCode1 || !langCode2) return false; - return parentLanguage(langCode2) === langCode1; + return langCode1 === langCode2 || parentLanguage(langCode2) === langCode1; } export function parentLanguage(langCode: string | null) { diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index a742a13ddc..a25e52ef8f 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -1,9 +1,12 @@ -import { markRaw, ref } from "vue"; +import { markRaw } from "vue"; import type { ApiTypes, entities } from "firefish-js"; import { isSignedIn, me } from "./me"; import { Storage } from "./pizzax"; import type { NoteVisibility } from "@/types/note"; +// biome-ignore lint/suspicious/noExplicitAny: <explanation> +type TODO = any; + export const postFormActions: { title: string; handler: (from, update) => void | Promise<void>; @@ -152,7 +155,7 @@ export const defaultStore = markRaw( type: string; size: "verySmall" | "small" | "medium" | "large" | "veryLarge"; black: boolean; - props: Record<string, any>; + props: Record<string, TODO>; }[], }, widgets: { @@ -161,7 +164,7 @@ export const defaultStore = markRaw( name: string; id: string; place: string | null; - data: Record<string, any>; + data: Record<string, TODO>; }[], }, tl: { @@ -465,109 +468,6 @@ export const defaultStore = markRaw( }), ); -// TODO: 他のタブと永続化されたstateを同期 +import ColdStore from "./cold-store"; -const PREFIX = "miux:"; - -interface Plugin { - id: string; - name: string; - active: boolean; - configData: Record<string, any>; - token: string; - ast: any[]; -} - -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"; - -export class ColdDeviceStorage { - public static default = { - lightTheme, - darkTheme, - syncDeviceDarkMode: true, - plugins: [] as Plugin[], - mediaVolume: 0.5, - vibrate: false, - sound_masterVolume: 0.3, - sound_note: { type: "none", volume: 0 }, - sound_noteMy: { type: "syuilo/up", volume: 1 }, - sound_notification: { type: "syuilo/pope2", volume: 1 }, - sound_chat: { type: "syuilo/pope1", volume: 1 }, - sound_chatBg: { type: "syuilo/waon", volume: 1 }, - sound_antenna: { type: "syuilo/triple", volume: 1 }, - sound_channel: { type: "syuilo/square-pico", volume: 1 }, - }; - - public static watchers = []; - - public static get<T extends keyof typeof ColdDeviceStorage.default>( - key: T, - ): (typeof ColdDeviceStorage.default)[T] { - // TODO: indexedDBにする - // ただしその際はnullチェックではなくキー存在チェックにしないとダメ - // (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある) - const value = localStorage.getItem(PREFIX + key); - if (value == null) { - return ColdDeviceStorage.default[key]; - } else { - return JSON.parse(value); - } - } - - public static set<T extends keyof typeof ColdDeviceStorage.default>( - key: T, - value: (typeof ColdDeviceStorage.default)[T], - ): void { - // 呼び出し側のバグ等で undefined が来ることがある - // undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 - if (value === undefined) { - console.error(`attempt to store undefined value for key '${key}'`); - return; - } - - localStorage.setItem(PREFIX + key, JSON.stringify(value)); - - for (const watcher of this.watchers) { - if (watcher.key === key) watcher.callback(value); - } - } - - public static watch(key, callback) { - this.watchers.push({ key, callback }); - } - - // TODO: VueのcustomRef使うと良い感じになるかも - public static ref<T extends keyof typeof ColdDeviceStorage.default>(key: T) { - const v = ColdDeviceStorage.get(key); - const r = ref(v); - // TODO: このままではwatcherがリークするので開放する方法を考える - this.watch(key, (v) => { - r.value = v; - }); - return r; - } - - /** - * 特定のキーの、簡易的なgetter/setterを作ります - * 主にvue場で設定コントロールのmodelとして使う用 - */ - public static makeGetterSetter< - K extends keyof typeof ColdDeviceStorage.default, - >(key: K) { - // TODO: VueのcustomRef使うと良い感じになるかも - const valueRef = ColdDeviceStorage.ref(key); - return { - get: () => { - return valueRef.value; - }, - set: (value: unknown) => { - const val = value; - ColdDeviceStorage.set(key, val); - }, - }; - } -} +export const ColdDeviceStorage = ColdStore; diff --git a/packages/client/src/stream.ts b/packages/client/src/stream.ts index 89dda63f08..b620c60c9d 100644 --- a/packages/client/src/stream.ts +++ b/packages/client/src/stream.ts @@ -31,6 +31,7 @@ export function reloadStream() { isReloading = true; stream.close(); + // biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally stream.once("_connected_", () => (isReloading = false)); stream.stream.reconnect(); isReloading = false; diff --git a/packages/client/src/theme-store.ts b/packages/client/src/theme-store.ts index bc344b339d..6f4616878f 100644 --- a/packages/client/src/theme-store.ts +++ b/packages/client/src/theme-store.ts @@ -17,7 +17,8 @@ export async function fetchThemes(): Promise<void> { key: "themes", }); localStorage.setItem(lsCacheKey, JSON.stringify(themes)); - } catch (err) { + // biome-ignore lint/suspicious/noExplicitAny: Safely any + } catch (err: any) { if (err.code === "NO_SUCH_KEY") return; throw err; } diff --git a/packages/client/src/types/page.ts b/packages/client/src/types/page.ts new file mode 100644 index 0000000000..30750e104d --- /dev/null +++ b/packages/client/src/types/page.ts @@ -0,0 +1,78 @@ +import type { TypeUtils } from "firefish-js"; + +export type BasePageContent = { + name: string; +}; + +export type PageContentTextInput = BasePageContent & { + type: "textInput"; + default: string; +}; + +export type PageContentTextareaInput = BasePageContent & { + type: "textareaInput"; + default?: string; +}; + +export type PageContentNumberInput = BasePageContent & { + type: "numberInput"; + default?: number; +}; + +export type PageContentSwitch = BasePageContent & { + type: "switch"; + default?: boolean; +}; +export type PageContentCounter = BasePageContent & { + type: "counter"; + default?: number; +}; + +export type PageContentRadioButton = BasePageContent & { + type: "radioButton"; + default?: string; +}; + +export type PageContentChildren = + | PageContentTextInput + | PageContentTextareaInput + | PageContentNumberInput + | PageContentSwitch + | PageContentCounter + | PageContentRadioButton; + +export type PageContentParent = { + type: "parent"; + children: PageContentChildren[]; +}; + +export type PageContent = PageContentParent | PageContentChildren; + +export type GetPageVar<T extends PageContentChildren> = { + name: string; + type: TypeUtils.NonUndefinedAble<T["default"]> extends string + ? "string" + : TypeUtils.NonUndefinedAble<T["default"]> extends boolean + ? "boolean" + : TypeUtils.NonUndefinedAble<T["default"]> extends number + ? "number" + : never; + value: TypeUtils.NonUndefinedAble<T["default"]>; +}; + +export type PageVar = + | { + name: string; + type: "string"; + value: string; + } + | { + name: string; + type: "boolean"; + value: boolean; + } + | { + name: string; + type: "number"; + value: number; + }; diff --git a/packages/client/src/types/relation.ts b/packages/client/src/types/relation.ts new file mode 100644 index 0000000000..1f4703f52f --- /dev/null +++ b/packages/client/src/types/relation.ts @@ -0,0 +1,5 @@ +export type Predicate<T> = (a: T) => boolean; + +export type Relation<T, U> = (a: T, b: U) => boolean; + +export type EndoRelation<T> = Relation<T, T>; diff --git a/packages/firefish-js/.swcrc b/packages/firefish-js/.swcrc index 89800c7b67..f398cdce5c 100644 --- a/packages/firefish-js/.swcrc +++ b/packages/firefish-js/.swcrc @@ -17,7 +17,7 @@ }, "minify": false, "module": { - "type": "commonjs", + "type": "es6", "strict": true, "resolveFully": true } diff --git a/packages/firefish-js/package.json b/packages/firefish-js/package.json index 01e5487988..b6512b1b79 100644 --- a/packages/firefish-js/package.json +++ b/packages/firefish-js/package.json @@ -4,6 +4,7 @@ "description": "Firefish SDK for JavaScript", "homepage": "https://firefish.dev/firefish/firefish/-/tree/develop/packages/firefish-js", "main": "./built/index.js", + "type": "module", "types": "./src/index.ts", "license": "MIT", "scripts": { @@ -35,7 +36,7 @@ "typescript": "5.4.5" }, "files": [ - "built" + "built", "src" ], "dependencies": { "eventemitter3": "5.0.1", diff --git a/packages/firefish-js/src/api.ts b/packages/firefish-js/src/api.ts index 4a7e1f0b64..5553c1feeb 100644 --- a/packages/firefish-js/src/api.ts +++ b/packages/firefish-js/src/api.ts @@ -1,4 +1,4 @@ -import type { Endpoints } from "./api.types"; +import type { Endpoints } from "./api.types.js"; const MK_API_ERROR = Symbol(); @@ -7,10 +7,12 @@ export type APIError = { code: string; message: string; kind: "client" | "server"; - info: Record<string, any>; + info: Record<string, unknown>; }; -export function isAPIError(reason: any): reason is APIError { +// biome-ignore lint/suspicious/noExplicitAny: used it intentially +type ExplicitlyUsedAny = any; +export function isAPIError(reason: ExplicitlyUsedAny): reason is APIError { return reason[MK_API_ERROR] === true; } @@ -24,7 +26,7 @@ export type FetchLike = ( }, ) => Promise<{ status: number; - json(): Promise<any>; + json(): Promise<ExplicitlyUsedAny>; }>; type IsNeverType<T> = [T] extends [never] ? true : false; @@ -36,7 +38,10 @@ type IsCaseMatched< P extends Endpoints[E]["req"], C extends number, > = IsNeverType< - StrictExtract<Endpoints[E]["res"]["$switch"]["$cases"][C], [P, any]> + StrictExtract< + Endpoints[E]["res"]["$switch"]["$cases"][C], + [P, ExplicitlyUsedAny] + > > extends false ? true : false; @@ -45,7 +50,10 @@ type GetCaseResult< E extends keyof Endpoints, P extends Endpoints[E]["req"], C extends number, -> = StrictExtract<Endpoints[E]["res"]["$switch"]["$cases"][C], [P, any]>[1]; +> = StrictExtract< + Endpoints[E]["res"]["$switch"]["$cases"][C], + [P, ExplicitlyUsedAny] +>[1]; export class APIClient { public origin: string; @@ -70,7 +78,7 @@ export class APIClient { credential?: string | null | undefined, ): Promise< Endpoints[E]["res"] extends { - $switch: { $cases: [any, any][]; $default: any }; + $switch: { $cases: [unknown, unknown][]; $default: unknown }; } ? IsCaseMatched<E, P, 0> extends true ? GetCaseResult<E, P, 0> diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts index 1ee94b9954..c458e46a09 100644 --- a/packages/firefish-js/src/api.types.ts +++ b/packages/firefish-js/src/api.types.ts @@ -38,7 +38,7 @@ import type { UserList, UserLite, UserSorting, -} from "./entities"; +} from "./entities.js"; import type * as consts from "./consts"; @@ -263,9 +263,9 @@ export type Endpoints = { // clips "clips/add-note": { req: TODO; res: TODO }; - "clips/create": { req: TODO; res: TODO }; + "clips/create": { req: TODO; res: Clip }; "clips/delete": { req: { clipId: Clip["id"] }; res: null }; - "clips/list": { req: TODO; res: TODO }; + "clips/list": { req: TODO; res: Clip[] }; "clips/notes": { req: TODO; res: TODO }; "clips/show": { req: TODO; res: TODO }; "clips/update": { req: TODO; res: TODO }; @@ -362,6 +362,16 @@ export type Endpoints = { res: DriveFile[]; }; + "email-address/available": { + req: { + emailAddress: string; + }; + res: { + available?: boolean; + reason: string | null; + }; + }; + // endpoint endpoint: { req: { endpoint: string }; @@ -738,6 +748,18 @@ export type Endpoints = { }; res: Note[]; }; + "notes/thread-muting/create": { + req: { + noteId: Note["id"]; + }; + res: null; + }; + "notes/thread-muting/delete": { + req: { + noteId: Note["id"]; + }; + res: null; + }; "notes/hybrid-timeline": { req: { limit?: number; @@ -758,6 +780,12 @@ export type Endpoints = { }; res: Note[]; }; + "notes/make-private": { + req: { + noteId: Note["id"]; + }; + res: null; + }; "notes/mentions": { req: { following?: boolean; @@ -899,6 +927,16 @@ export type Endpoints = { // promo "promo/read": { req: TODO; res: TODO }; + // release + release: { + req: null; + res: { + version: string; + notes: string; + screenshots: string[]; + }; + }; + // request-reset-password "request-reset-password": { req: { username: string; email: string }; @@ -921,8 +959,36 @@ export type Endpoints = { // ck specific "latest-version": { req: NoParams; res: TODO }; + // signin + signin: { + req: { + username: string; + password: string; + "hcaptcha-response"?: null | string; + "g-recaptcha-response"?: null | string; + }; + res: + | { + id: User["id"]; + i: string; + } + | { + challenge: string; + challengeId: string; + securityKeys: { + id: string; + }[]; + }; + }; + // sw "sw/register": { req: TODO; res: TODO }; + "sw/unregister": { + req: { + endpoint: string; + }; + res: null; + }; // username "username/available": { diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index 9ab3c2fff6..93e11285ba 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -1,4 +1,5 @@ -import type * as consts from "./consts"; +import type * as consts from "./consts.js"; +import type { Packed } from "./misc/schema.js"; export type ID = string; export type DateString = string; @@ -116,6 +117,7 @@ export type MeDetailed = UserDetailed & { preventAiLearning: boolean; receiveAnnouncementEmail: boolean; usePasswordLessLogin: boolean; + token: string; [other: string]: any; }; @@ -481,7 +483,7 @@ export type Announcement = { imageUrl: string | null; isRead?: boolean; isGoodNews: boolean; - showPopUp: boolean; + showPopup: boolean; }; export type Antenna = { @@ -512,7 +514,7 @@ export type AuthSession = { export type Ad = TODO; -export type Clip = TODO; +export type Clip = Packed<"Clip">; export type NoteFavorite = { id: ID; diff --git a/packages/firefish-js/src/index.ts b/packages/firefish-js/src/index.ts index 3398ed8a2e..b5baa85989 100644 --- a/packages/firefish-js/src/index.ts +++ b/packages/firefish-js/src/index.ts @@ -1,14 +1,17 @@ -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"; -import type * as TypeUtils from "./type-utils"; +import * as acct from "./acct.js"; +import type { Acct } from "./acct.js"; +import type { Endpoints } from "./api.types.js"; +import type * as ApiTypes from "./api.types.js"; +import * as consts from "./consts.js"; +import Stream, { Connection } from "./streaming.js"; +import * as StreamTypes from "./streaming.types.js"; +import type * as TypeUtils from "./type-utils.js"; + +import type * as SchemaTypes from "./misc/schema.js"; +import * as Schema from "./misc/schema.js"; export { - Endpoints, + type Endpoints, type ApiTypes, Stream, Connection as ChannelConnection, @@ -16,6 +19,8 @@ export { acct, type Acct, type TypeUtils, + Schema, + type SchemaTypes, }; export const permissions = consts.permissions; @@ -26,9 +31,12 @@ export const languages = consts.languages; export const ffVisibility = consts.ffVisibility; export const instanceSortParam = consts.instanceSortParam; +import { langmap, type PostLanguage } from "./misc/langmap.js"; +export { langmap, type PostLanguage }; + // api extractor not supported yet //export * as api from './api'; //export * as entities from './entities'; -import * as api from "./api"; -import * as entities from "./entities"; +import * as api from "./api.js"; +import * as entities from "./entities.js"; export { api, entities }; diff --git a/packages/backend/src/misc/langmap.ts b/packages/firefish-js/src/misc/langmap.ts similarity index 98% rename from packages/backend/src/misc/langmap.ts rename to packages/firefish-js/src/misc/langmap.ts index 2506a36151..16d169d914 100644 --- a/packages/backend/src/misc/langmap.ts +++ b/packages/firefish-js/src/misc/langmap.ts @@ -1,4 +1,3 @@ -// TODO: sharedに置いてバックエンドのと統合したい export const iso639Langs1 = { af: { nativeName: "Afrikaans", diff --git a/packages/firefish-js/src/misc/schema.ts b/packages/firefish-js/src/misc/schema.ts new file mode 100644 index 0000000000..811190e3e1 --- /dev/null +++ b/packages/firefish-js/src/misc/schema.ts @@ -0,0 +1,241 @@ +import { + packedUserLiteSchema, + packedUserDetailedNotMeOnlySchema, + packedMeDetailedOnlySchema, + packedUserDetailedNotMeSchema, + packedMeDetailedSchema, + packedUserDetailedSchema, + packedUserSchema, +} from "../schema/user.js"; +import { packedNoteSchema } from "../schema/note.js"; +import { packedUserListSchema } from "../schema/user-list.js"; +import { packedAppSchema } from "../schema/app.js"; +import { packedMessagingMessageSchema } from "../schema/messaging-message.js"; +import { packedNotificationSchema } from "../schema/notification.js"; +import { packedDriveFileSchema } from "../schema/drive-file.js"; +import { packedDriveFolderSchema } from "../schema/drive-folder.js"; +import { packedFollowingSchema } from "../schema/following.js"; +import { packedMutingSchema } from "../schema/muting.js"; +import { packedRenoteMutingSchema } from "../schema/renote-muting.js"; +import { packedReplyMutingSchema } from "../schema/reply-muting.js"; +import { packedBlockingSchema } from "../schema/blocking.js"; +import { packedNoteReactionSchema } from "../schema/note-reaction.js"; +import { packedHashtagSchema } from "../schema/hashtag.js"; +import { packedPageSchema } from "../schema/page.js"; +import { packedUserGroupSchema } from "../schema/user-group.js"; +import { packedNoteFavoriteSchema } from "../schema/note-favorite.js"; +import { packedChannelSchema } from "../schema/channel.js"; +import { packedAntennaSchema } from "../schema/antenna.js"; +import { packedClipSchema } from "../schema/clip.js"; +import { packedFederationInstanceSchema } from "../schema/federation-instance.js"; +import { packedQueueCountSchema } from "../schema/queue.js"; +import { packedGalleryPostSchema } from "../schema/gallery-post.js"; +import { packedEmojiSchema } from "../schema/emoji.js"; +import { packedNoteEdit } from "../schema/note-edit.js"; +import { packedNoteFileSchema } from "../schema/note-file.js"; +import { packedAbuseUserReportSchema } from "../schema/abuse-user-report.js"; + +export const refs = { + AbuseUserReport: packedAbuseUserReportSchema, + UserLite: packedUserLiteSchema, + UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema, + MeDetailedOnly: packedMeDetailedOnlySchema, + UserDetailedNotMe: packedUserDetailedNotMeSchema, + MeDetailed: packedMeDetailedSchema, + UserDetailed: packedUserDetailedSchema, + User: packedUserSchema, + + UserList: packedUserListSchema, + UserGroup: packedUserGroupSchema, + App: packedAppSchema, + MessagingMessage: packedMessagingMessageSchema, + Note: packedNoteSchema, + NoteFile: packedNoteFileSchema, + NoteEdit: packedNoteEdit, + NoteReaction: packedNoteReactionSchema, + NoteFavorite: packedNoteFavoriteSchema, + Notification: packedNotificationSchema, + DriveFile: packedDriveFileSchema, + DriveFolder: packedDriveFolderSchema, + Following: packedFollowingSchema, + Muting: packedMutingSchema, + RenoteMuting: packedRenoteMutingSchema, + ReplyMuting: packedReplyMutingSchema, + Blocking: packedBlockingSchema, + Hashtag: packedHashtagSchema, + Page: packedPageSchema, + Channel: packedChannelSchema, + QueueCount: packedQueueCountSchema, + Antenna: packedAntennaSchema, + Clip: packedClipSchema, + FederationInstance: packedFederationInstanceSchema, + GalleryPost: packedGalleryPostSchema, + Emoji: packedEmojiSchema, +}; + +// biome-ignore lint/suspicious/noExplicitAny: used it intentially +type ExplicitlyUsedAny = any; + +export type Packed<x extends keyof typeof refs> = SchemaType<(typeof refs)[x]>; + +type TypeStringef = + | "null" + | "boolean" + | "integer" + | "number" + | "string" + | "array" + | "object" + | "any"; +type StringDefToType<T extends TypeStringef> = T extends "null" + ? null + : T extends "boolean" + ? boolean + : T extends "integer" + ? number + : T extends "number" + ? number + : T extends "string" + ? string | Date + : T extends "array" + ? ReadonlyArray<ExplicitlyUsedAny> + : T extends "object" + ? Record<string, ExplicitlyUsedAny> + : ExplicitlyUsedAny; + +// https://swagger.io/specification/?sbsearch=optional#schema-object +type OfSchema = { + readonly anyOf?: ReadonlyArray<Schema>; + readonly oneOf?: ReadonlyArray<Schema>; + readonly allOf?: ReadonlyArray<Schema>; +}; + +export interface Schema extends OfSchema { + readonly type?: TypeStringef; + readonly nullable?: boolean; + readonly optional?: boolean; + readonly items?: Schema; + readonly properties?: Obj; + readonly required?: ReadonlyArray< + Extract<keyof NonNullable<this["properties"]>, string> + >; + readonly description?: string; + readonly example?: ExplicitlyUsedAny; + readonly format?: string; + readonly ref?: keyof typeof refs; + readonly enum?: ReadonlyArray<string>; + readonly default?: + | (this["type"] extends TypeStringef + ? StringDefToType<this["type"]> + : ExplicitlyUsedAny) + | null; + readonly maxLength?: number; + readonly minLength?: number; + readonly maximum?: number; + readonly minimum?: number; + readonly pattern?: string; +} + +type RequiredPropertyNames<s extends Obj> = { + [K in keyof s]: // K is not optional + s[K]["optional"] extends false + ? K + : // K has default value + s[K]["default"] extends + | null + | string + | number + | boolean + | Record<string, unknown> + ? K + : never; +}[keyof s]; + +export type Obj = Record<string, Schema>; + +// https://github.com/misskey-dev/misskey/issues/8535 +// To avoid excessive stack depth error, +// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it). +export type ObjType< + s extends Obj, + RequiredProps extends keyof s, +> = UnionToIntersection< + { + -readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]>; + } & { + -readonly [R in RequiredProps]-?: SchemaType<s[R]>; + } & { + -readonly [P in keyof s]?: SchemaType<s[P]>; + } +>; + +type NullOrUndefined<p extends Schema, T> = + | (p["nullable"] extends true ? null : never) + | (p["optional"] extends true ? undefined : never) + | T; + +// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection +// Get intersection from union +type UnionToIntersection<U> = ( + U extends ExplicitlyUsedAny + ? (k: U) => void + : never +) extends (k: infer I) => void + ? I + : never; + +// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 +// To get union, we use `Foo extends ExplicitlyUsedAny ? Hoge<Foo> : never` +type UnionSchemaType< + a extends readonly ExplicitlyUsedAny[], + X extends Schema = a[number], +> = X extends ExplicitlyUsedAny ? SchemaType<X> : never; +type ArrayUnion<T> = T extends ExplicitlyUsedAny ? Array<T> : never; + +export type SchemaTypeDef<p extends Schema> = p["type"] extends "null" + ? null + : p["type"] extends "integer" + ? number + : p["type"] extends "number" + ? number + : p["type"] extends "string" + ? p["enum"] extends readonly string[] + ? p["enum"][number] + : p["format"] extends "date-time" + ? string + : // Dateにする?? + string + : p["type"] extends "boolean" + ? boolean + : p["type"] extends "object" + ? p["ref"] extends keyof typeof refs + ? Packed<p["ref"]> + : p["properties"] extends NonNullable<Obj> + ? ObjType<p["properties"], NonNullable<p["required"]>[number]> + : p["anyOf"] extends ReadonlyArray<Schema> + ? UnionSchemaType<p["anyOf"]> & + Partial<UnionToIntersection<UnionSchemaType<p["anyOf"]>>> + : p["allOf"] extends ReadonlyArray<Schema> + ? UnionToIntersection<UnionSchemaType<p["allOf"]>> + : ExplicitlyUsedAny + : p["type"] extends "array" + ? p["items"] extends OfSchema + ? p["items"]["anyOf"] extends ReadonlyArray<Schema> + ? UnionSchemaType<NonNullable<p["items"]["anyOf"]>>[] + : p["items"]["oneOf"] extends ReadonlyArray<Schema> + ? ArrayUnion< + UnionSchemaType<NonNullable<p["items"]["oneOf"]>> + > + : p["items"]["allOf"] extends ReadonlyArray<Schema> + ? UnionToIntersection< + UnionSchemaType<NonNullable<p["items"]["allOf"]>> + >[] + : never + : p["items"] extends NonNullable<Schema> + ? SchemaTypeDef<p["items"]>[] + : ExplicitlyUsedAny[] + : p["oneOf"] extends ReadonlyArray<Schema> + ? UnionSchemaType<p["oneOf"]> + : ExplicitlyUsedAny; + +export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>; diff --git a/packages/backend/src/models/schema/abuse-user-report.ts b/packages/firefish-js/src/schema/abuse-user-report.ts similarity index 100% rename from packages/backend/src/models/schema/abuse-user-report.ts rename to packages/firefish-js/src/schema/abuse-user-report.ts diff --git a/packages/backend/src/models/schema/antenna.ts b/packages/firefish-js/src/schema/antenna.ts similarity index 100% rename from packages/backend/src/models/schema/antenna.ts rename to packages/firefish-js/src/schema/antenna.ts diff --git a/packages/backend/src/models/schema/app.ts b/packages/firefish-js/src/schema/app.ts similarity index 100% rename from packages/backend/src/models/schema/app.ts rename to packages/firefish-js/src/schema/app.ts diff --git a/packages/backend/src/models/schema/blocking.ts b/packages/firefish-js/src/schema/blocking.ts similarity index 100% rename from packages/backend/src/models/schema/blocking.ts rename to packages/firefish-js/src/schema/blocking.ts diff --git a/packages/backend/src/models/schema/channel.ts b/packages/firefish-js/src/schema/channel.ts similarity index 100% rename from packages/backend/src/models/schema/channel.ts rename to packages/firefish-js/src/schema/channel.ts diff --git a/packages/backend/src/models/schema/clip.ts b/packages/firefish-js/src/schema/clip.ts similarity index 100% rename from packages/backend/src/models/schema/clip.ts rename to packages/firefish-js/src/schema/clip.ts diff --git a/packages/backend/src/models/schema/drive-file.ts b/packages/firefish-js/src/schema/drive-file.ts similarity index 100% rename from packages/backend/src/models/schema/drive-file.ts rename to packages/firefish-js/src/schema/drive-file.ts diff --git a/packages/backend/src/models/schema/drive-folder.ts b/packages/firefish-js/src/schema/drive-folder.ts similarity index 100% rename from packages/backend/src/models/schema/drive-folder.ts rename to packages/firefish-js/src/schema/drive-folder.ts diff --git a/packages/backend/src/models/schema/emoji.ts b/packages/firefish-js/src/schema/emoji.ts similarity index 100% rename from packages/backend/src/models/schema/emoji.ts rename to packages/firefish-js/src/schema/emoji.ts diff --git a/packages/backend/src/models/schema/federation-instance.ts b/packages/firefish-js/src/schema/federation-instance.ts similarity index 97% rename from packages/backend/src/models/schema/federation-instance.ts rename to packages/firefish-js/src/schema/federation-instance.ts index 338e079e28..9ef0d337b5 100644 --- a/packages/backend/src/models/schema/federation-instance.ts +++ b/packages/firefish-js/src/schema/federation-instance.ts @@ -1,5 +1,3 @@ -import { config } from "@/config.js"; - export const packedFederationInstanceSchema = { type: "object", properties: { @@ -83,7 +81,7 @@ export const packedFederationInstanceSchema = { type: "string", optional: false, nullable: true, - example: config.version, + example: "20240424", }, openRegistrations: { type: "boolean", diff --git a/packages/backend/src/models/schema/following.ts b/packages/firefish-js/src/schema/following.ts similarity index 100% rename from packages/backend/src/models/schema/following.ts rename to packages/firefish-js/src/schema/following.ts diff --git a/packages/backend/src/models/schema/gallery-post.ts b/packages/firefish-js/src/schema/gallery-post.ts similarity index 100% rename from packages/backend/src/models/schema/gallery-post.ts rename to packages/firefish-js/src/schema/gallery-post.ts diff --git a/packages/backend/src/models/schema/hashtag.ts b/packages/firefish-js/src/schema/hashtag.ts similarity index 100% rename from packages/backend/src/models/schema/hashtag.ts rename to packages/firefish-js/src/schema/hashtag.ts diff --git a/packages/backend/src/models/schema/messaging-message.ts b/packages/firefish-js/src/schema/messaging-message.ts similarity index 100% rename from packages/backend/src/models/schema/messaging-message.ts rename to packages/firefish-js/src/schema/messaging-message.ts diff --git a/packages/backend/src/models/schema/muting.ts b/packages/firefish-js/src/schema/muting.ts similarity index 100% rename from packages/backend/src/models/schema/muting.ts rename to packages/firefish-js/src/schema/muting.ts diff --git a/packages/backend/src/models/schema/note-edit.ts b/packages/firefish-js/src/schema/note-edit.ts similarity index 100% rename from packages/backend/src/models/schema/note-edit.ts rename to packages/firefish-js/src/schema/note-edit.ts diff --git a/packages/backend/src/models/schema/note-favorite.ts b/packages/firefish-js/src/schema/note-favorite.ts similarity index 100% rename from packages/backend/src/models/schema/note-favorite.ts rename to packages/firefish-js/src/schema/note-favorite.ts diff --git a/packages/backend/src/models/schema/note-file.ts b/packages/firefish-js/src/schema/note-file.ts similarity index 100% rename from packages/backend/src/models/schema/note-file.ts rename to packages/firefish-js/src/schema/note-file.ts diff --git a/packages/backend/src/models/schema/note-reaction.ts b/packages/firefish-js/src/schema/note-reaction.ts similarity index 100% rename from packages/backend/src/models/schema/note-reaction.ts rename to packages/firefish-js/src/schema/note-reaction.ts diff --git a/packages/backend/src/models/schema/note.ts b/packages/firefish-js/src/schema/note.ts similarity index 94% rename from packages/backend/src/models/schema/note.ts rename to packages/firefish-js/src/schema/note.ts index 6064919960..73e85e6f0d 100644 --- a/packages/backend/src/models/schema/note.ts +++ b/packages/firefish-js/src/schema/note.ts @@ -1,4 +1,4 @@ -import { langmap } from "@/misc/langmap.js"; +import { langmap } from "../misc/langmap.js"; export const packedNoteSchema = { type: "object", @@ -208,15 +208,5 @@ export const packedNoteSchema = { optional: true, nullable: true, }, - myRenoteCount: { - type: "number", - optional: true, - nullable: false, - }, - quoteCount: { - type: "number", - optional: false, - nullable: false, - }, }, } as const; diff --git a/packages/backend/src/models/schema/notification.ts b/packages/firefish-js/src/schema/notification.ts similarity index 96% rename from packages/backend/src/models/schema/notification.ts rename to packages/firefish-js/src/schema/notification.ts index 97fd16339c..dec921a8a7 100644 --- a/packages/backend/src/models/schema/notification.ts +++ b/packages/firefish-js/src/schema/notification.ts @@ -1,4 +1,4 @@ -import { notificationTypes } from "@/types.js"; +import { notificationTypes } from "../consts.js"; export const packedNotificationSchema = { type: "object", diff --git a/packages/backend/src/models/schema/page.ts b/packages/firefish-js/src/schema/page.ts similarity index 100% rename from packages/backend/src/models/schema/page.ts rename to packages/firefish-js/src/schema/page.ts diff --git a/packages/backend/src/models/schema/queue.ts b/packages/firefish-js/src/schema/queue.ts similarity index 100% rename from packages/backend/src/models/schema/queue.ts rename to packages/firefish-js/src/schema/queue.ts diff --git a/packages/backend/src/models/schema/renote-muting.ts b/packages/firefish-js/src/schema/renote-muting.ts similarity index 100% rename from packages/backend/src/models/schema/renote-muting.ts rename to packages/firefish-js/src/schema/renote-muting.ts diff --git a/packages/backend/src/models/schema/reply-muting.ts b/packages/firefish-js/src/schema/reply-muting.ts similarity index 100% rename from packages/backend/src/models/schema/reply-muting.ts rename to packages/firefish-js/src/schema/reply-muting.ts diff --git a/packages/backend/src/models/schema/user-group.ts b/packages/firefish-js/src/schema/user-group.ts similarity index 100% rename from packages/backend/src/models/schema/user-group.ts rename to packages/firefish-js/src/schema/user-group.ts diff --git a/packages/backend/src/models/schema/user-list.ts b/packages/firefish-js/src/schema/user-list.ts similarity index 100% rename from packages/backend/src/models/schema/user-list.ts rename to packages/firefish-js/src/schema/user-list.ts diff --git a/packages/backend/src/models/schema/user.ts b/packages/firefish-js/src/schema/user.ts similarity index 100% rename from packages/backend/src/models/schema/user.ts rename to packages/firefish-js/src/schema/user.ts diff --git a/packages/firefish-js/src/streaming.ts b/packages/firefish-js/src/streaming.ts index 58491bc8c0..0fcd0c7498 100644 --- a/packages/firefish-js/src/streaming.ts +++ b/packages/firefish-js/src/streaming.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "eventemitter3"; import ReconnectingWebsocket from "reconnecting"; -import type { BroadcastEvents, Channels } from "./streaming.types"; +import type { BroadcastEvents, Channels } from "./streaming.types.js"; function autobind(instance: any): void { const prototype = Object.getPrototypeOf(instance); diff --git a/packages/firefish-js/src/streaming.types.ts b/packages/firefish-js/src/streaming.types.ts index 5b81780271..ca4aa3396d 100644 --- a/packages/firefish-js/src/streaming.types.ts +++ b/packages/firefish-js/src/streaming.types.ts @@ -12,7 +12,7 @@ import type { UserGroup, UserLite, } from "./entities"; -import type { Connection } from "./streaming"; +import type { Connection } from "./streaming.js"; type FIXME = any; diff --git a/packages/firefish-js/src/type-utils.ts b/packages/firefish-js/src/type-utils.ts index bf8297487b..92695709b5 100644 --- a/packages/firefish-js/src/type-utils.ts +++ b/packages/firefish-js/src/type-utils.ts @@ -1,7 +1,9 @@ -import type { Endpoints } from "./api.types"; +import type { Endpoints } from "./api.types.js"; -type PropertyOfType<Type, U> = { +export type PropertyOfType<Type, U> = { [K in keyof Type]: Type[K] extends U ? K : never; }[keyof Type]; export type EndpointsOf<T> = PropertyOfType<Endpoints, { res: T }>; + +export type NonUndefinedAble<T> = T extends undefined ? never : T; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b466c2336..56657373e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,6 +141,9 @@ importers: file-type: specifier: 19.0.0 version: 19.0.0 + firefish-js: + specifier: workspace:* + version: link:../firefish-js fluent-ffmpeg: specifier: 2.1.2 version: 2.1.2