Merge branch 'develop' into feat/schedule-create

This commit is contained in:
naskya 2024-05-16 17:32:42 +09:00
commit 452728b352
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
85 changed files with 840 additions and 988 deletions

View file

@ -98,6 +98,7 @@ test:build:backend_ts_only:
changes: changes:
paths: paths:
- packages/backend/**/* - packages/backend/**/*
- packages/firefish-js/**/*
- packages/megalodon/**/* - packages/megalodon/**/*
when: always when: always
before_script: before_script:
@ -115,7 +116,7 @@ test:build:backend_ts_only:
- psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga' - psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga'
script: script:
- pnpm install --frozen-lockfile - 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 - pnpm run migrate
test:build:client_only: test:build:client_only:

View file

@ -55,6 +55,7 @@
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "19.0.0", "file-type": "19.0.0",
"firefish-js": "workspace:*",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "14.2.1", "got": "14.2.1",

View file

@ -1,234 +1,7 @@
import { // TODO: use firefish-js
packedUserLiteSchema, import { Schema as _Schema } from "firefish-js";
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";
export const refs = { export const refs = _Schema.refs;
AbuseUserReport: packedAbuseUserReportSchema, export type Packed<T extends keyof typeof refs> = _Schema.Packed<T>;
UserLite: packedUserLiteSchema, export type Schema = _Schema.Schema;
UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema, export type SchemaType<P extends _Schema.Schema> = _Schema.SchemaType<P>;
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>>;

View file

@ -53,7 +53,7 @@ import { UserProfiles } from "@/models/index.js";
import { In } from "typeorm"; import { In } from "typeorm";
import { config } from "@/config.js"; import { config } from "@/config.js";
import { truncate } from "@/misc/truncate.js"; import { truncate } from "@/misc/truncate.js";
import { langmap } from "@/misc/langmap.js"; import { langmap } from "firefish-js";
import { inspect } from "node:util"; import { inspect } from "node:util";
export function validateNote(object: any, uri: string) { export function validateNote(object: any, uri: string) {

View file

@ -18,8 +18,8 @@ import { ApiError } from "@/server/api/error.js";
import define from "@/server/api/define.js"; import define from "@/server/api/define.js";
import { HOUR, genId } from "backend-rs"; import { HOUR, genId } from "backend-rs";
import { getNote } from "@/server/api/common/getters.js"; import { getNote } from "@/server/api/common/getters.js";
import { langmap } from "@/misc/langmap.js"; import { langmap } from "firefish-js";
import { createScheduledCreateNoteJob } from "@/queue"; import { createScheduledCreateNoteJob } from "@/queue/index.js";
export const meta = { export const meta = {
tags: ["notes"], tags: ["notes"],

View file

@ -33,7 +33,7 @@ import renderNote from "@/remote/activitypub/renderer/note.js";
import renderUpdate from "@/remote/activitypub/renderer/update.js"; import renderUpdate from "@/remote/activitypub/renderer/update.js";
import { deliverToRelays } from "@/services/relay.js"; import { deliverToRelays } from "@/services/relay.js";
// import { deliverQuestionUpdate } from "@/services/note/polls/update.js"; // import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
import { langmap } from "@/misc/langmap.js"; import { langmap } from "firefish-js";
export const meta = { export const meta = {
tags: ["notes"], tags: ["notes"],

View file

@ -1,7 +1,7 @@
import { ApiError } from "@/server/api/error.js"; import { ApiError } from "@/server/api/error.js";
import { getNote } from "@/server/api/common/getters.js"; import { getNote } from "@/server/api/common/getters.js";
import { translate } from "@/misc/translate.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"; import define from "@/server/api/define.js";
export const meta = { export const meta = {

View file

@ -63,7 +63,7 @@ import { db } from "@/db/postgre.js";
import { getActiveWebhooks } from "@/misc/webhook-cache.js"; import { getActiveWebhooks } from "@/misc/webhook-cache.js";
import { redisClient } from "@/db/redis.js"; import { redisClient } from "@/db/redis.js";
import { Mutex } from "redis-semaphore"; import { Mutex } from "redis-semaphore";
import { langmap } from "@/misc/langmap.js"; import { langmap } from "firefish-js";
import Logger from "@/services/logger.js"; import Logger from "@/services/logger.js";
import { inspect } from "node:util"; import { inspect } from "node:util";
import { toRustObject } from "@/prelude/undefined-to-null.js"; import { toRustObject } from "@/prelude/undefined-to-null.js";

View file

@ -7,6 +7,7 @@ import { alert, api, popup, popupMenu, waiting } from "@/os";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import { del, get, set } from "@/scripts/idb-proxy"; import { del, get, set } from "@/scripts/idb-proxy";
import { reloadChannel, unisonReload } from "@/scripts/unison-reload"; import { reloadChannel, unisonReload } from "@/scripts/unison-reload";
import type { MenuButton, MenuUser } from "./types/menu";
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
@ -16,7 +17,7 @@ export async function signOut() {
waiting(); waiting();
localStorage.removeItem("account"); localStorage.removeItem("account");
await removeAccount(me.id); await removeAccount(me!.id);
const accounts = await getAccounts(); const accounts = await getAccounts();
@ -26,12 +27,9 @@ export async function signOut() {
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker.ready;
const push = await registration.pushManager.getSubscription(); const push = await registration.pushManager.getSubscription();
if (push) { if (push) {
await fetch(`${apiUrl}/sw/unregister`, { await api("sw/unregister", {
method: "POST", endpoint: push.endpoint,
body: JSON.stringify({ i: me!.token, // FIXME: This parameter seems to be removable but I didn't test it
i: me.token,
endpoint: push.endpoint,
}),
}); });
} }
} }
@ -117,13 +115,13 @@ function showSuspendedDialog() {
export function updateAccount(accountData) { export function updateAccount(accountData) {
for (const [key, value] of Object.entries(accountData)) { for (const [key, value] of Object.entries(accountData)) {
me[key] = value; me![key] = value;
} }
localStorage.setItem("account", JSON.stringify(me)); localStorage.setItem("account", JSON.stringify(me));
} }
export async function refreshAccount() { export async function refreshAccount() {
const accountData = await fetchAccount(me.token); const accountData = await fetchAccount(me!.token);
return updateAccount(accountData); return updateAccount(accountData);
} }
@ -189,7 +187,7 @@ export async function openAccountMenu(
async function switchAccount(account: entities.UserDetailed) { async function switchAccount(account: entities.UserDetailed) {
const storedAccounts = await getAccounts(); 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); switchAccountWithToken(token);
} }
@ -198,15 +196,15 @@ export async function openAccountMenu(
} }
const storedAccounts = await getAccounts().then((accounts) => 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", { const accountsPromise = api("users/show", {
userIds: storedAccounts.map((x) => x.id), userIds: storedAccounts.map((x) => x.id),
}); });
function createItem(account: entities.UserDetailed) { function createItem(account: entities.UserDetailed): MenuUser {
return { return {
type: "user", type: "user" as const,
user: account, user: account,
active: opts.active != null ? opts.active === account.id : false, active: opts.active != null ? opts.active === account.id : false,
action: () => { action: () => {
@ -221,10 +219,14 @@ export async function openAccountMenu(
const accountItemPromises = storedAccounts.map( const accountItemPromises = storedAccounts.map(
(a) => (a) =>
new Promise((res) => { new Promise<MenuUser>((res) => {
accountsPromise.then((accounts) => { accountsPromise.then((accounts) => {
const account = accounts.find((x) => x.id === a.id); 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)); res(createItem(account));
}); });
}), }),
@ -233,74 +235,72 @@ export async function openAccountMenu(
if (opts.withExtraOperation) { if (opts.withExtraOperation) {
popupMenu( popupMenu(
[ [
...[ ...(isMobile ?? false
...(isMobile ?? false ? [
? [ {
{ type: "parent" as const,
type: "parent", icon: `${icon("ph-plus")}`,
icon: `${icon("ph-plus")}`, text: i18n.ts.addAccount,
text: i18n.ts.addAccount, children: [
children: [ {
{ text: i18n.ts.existingAccount,
text: i18n.ts.existingAccount, action: () => {
action: () => { showSigninDialog();
showSigninDialog();
},
}, },
{ },
text: i18n.ts.createAccount, {
action: () => { text: i18n.ts.createAccount,
createAccount(); action: () => {
}, createAccount();
}, },
], },
}, ],
] },
: [ ]
{ : [
type: "link", {
text: i18n.ts.profile, type: "link" as const,
to: `/@${me.username}`, text: i18n.ts.profile,
avatar: me, to: `/@${me!.username}`,
}, avatar: me!,
null, },
]), null,
...(opts.includeCurrentAccount ? [createItem(me)] : []), ]),
...accountItemPromises, ...(opts.includeCurrentAccount ? [createItem(me!)] : []),
...(isMobile ?? false ...accountItemPromises,
? [ ...(isMobile ?? false
null, ? [
{ null,
type: "link", {
text: i18n.ts.profile, type: "link" as const,
to: `/@${me.username}`, text: i18n.ts.profile,
avatar: me, to: `/@${me!.username}`,
}, avatar: me!,
] },
: [ ]
{ : [
type: "parent", {
icon: `${icon("ph-plus")}`, type: "parent" as const,
text: i18n.ts.addAccount, icon: `${icon("ph-plus")}`,
children: [ text: i18n.ts.addAccount,
{ children: [
text: i18n.ts.existingAccount, {
action: () => { text: i18n.ts.existingAccount,
showSigninDialog(); action: () => {
}, showSigninDialog();
}, },
{ },
text: i18n.ts.createAccount, {
action: () => { text: i18n.ts.createAccount,
createAccount(); action: () => {
}, createAccount();
}, },
], },
}, ],
]), },
], ]),
], ],
ev.currentTarget ?? ev.target, (ev.currentTarget ?? ev.target) as HTMLElement,
{ {
align: "left", align: "left",
}, },
@ -308,10 +308,10 @@ export async function openAccountMenu(
} else { } else {
popupMenu( popupMenu(
[ [
...(opts.includeCurrentAccount ? [createItem(me)] : []), ...(opts.includeCurrentAccount ? [createItem(me!)] : []),
...accountItemPromises, ...accountItemPromises,
], ],
ev.currentTarget ?? ev.target, (ev.currentTarget ?? ev.target) as HTMLElement,
{ {
align: "left", align: "left",
}, },

View file

@ -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,
};

View file

@ -1,5 +1,5 @@
<template> <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.root">
<div :class="$style.title"> <div :class="$style.title">
<MkSparkle v-if="isGoodNews">{{ title }}</MkSparkle> <MkSparkle v-if="isGoodNews">{{ title }}</MkSparkle>
@ -41,6 +41,10 @@ const props = defineProps<{
announcement: entities.Announcement; announcement: entities.Announcement;
}>(); }>();
const emit = defineEmits<{
closed: [];
}>();
const { id, text, title, imageUrl, isGoodNews } = props.announcement; const { id, text, title, imageUrl, isGoodNews } = props.announcement;
const modal = shallowRef<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();

View file

@ -182,7 +182,7 @@ export default {
const props = defineProps<{ const props = defineProps<{
type: string; type: string;
q: string | null; q: string | null;
textarea: HTMLTextAreaElement; textarea: HTMLTextAreaElement | HTMLInputElement;
close: () => void; close: () => void;
x: number; x: number;
y: number; y: number;
@ -435,7 +435,7 @@ onUpdated(() => {
onMounted(() => { onMounted(() => {
setPosition(); setPosition();
props.textarea.addEventListener("keydown", onKeydown); (props.textarea as HTMLTextAreaElement).addEventListener("keydown", onKeydown);
document.body.addEventListener("mousedown", onMousedown); document.body.addEventListener("mousedown", onMousedown);
nextTick(() => { nextTick(() => {
@ -453,7 +453,7 @@ onMounted(() => {
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
props.textarea.removeEventListener("keydown", onKeydown); (props.textarea as HTMLTextAreaElement).removeEventListener("keydown", onKeydown);
document.body.removeEventListener("mousedown", onMousedown); document.body.removeEventListener("mousedown", onMousedown);
}); });
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <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.root">
<p :class="$style.title"> <p :class="$style.title">
{{ i18n.ts.youHaveUnreadAnnouncements }} {{ i18n.ts.youHaveUnreadAnnouncements }}
@ -21,6 +21,10 @@ import MkModal from "@/components/MkModal.vue";
import MkButton from "@/components/MkButton.vue"; import MkButton from "@/components/MkButton.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
const emit = defineEmits<{
closed: [];
}>();
const modal = shallowRef<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
const checkAnnouncements = () => { const checkAnnouncements = () => {
modal.value!.close(); modal.value!.close();

View file

@ -852,13 +852,17 @@ function setLanguage() {
actions.push(null); actions.push(null);
} }
if (language.value != null) if (language.value != null && langmap[language.value] != null) {
actions.push({ actions.push({
text: langmap[language.value].nativeName, text: langmap[language.value].nativeName,
danger: false, danger: false,
active: true, active: true,
action: () => {}, action: () => {},
}); });
} else {
// Unrecognized language, set to null
language.value = null;
}
const langs = Object.keys(langmap); const langs = Object.keys(langmap);

View file

@ -160,7 +160,7 @@ const hCaptchaResponse = ref(null);
const reCaptchaResponse = ref(null); const reCaptchaResponse = ref(null);
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "login", v: any): void; login: [v: { id: string; i: string }];
}>(); }>();
const props = defineProps({ const props = defineProps({

View file

@ -30,7 +30,7 @@ withDefaults(
); );
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "done"): void; (ev: "done", res: { id: string; i: string }): void;
(ev: "closed"): void; (ev: "closed"): void;
(ev: "cancelled"): void; (ev: "cancelled"): void;
}>(); }>();
@ -39,11 +39,11 @@ const dialog = ref<InstanceType<typeof XModalWindow>>();
function onClose() { function onClose() {
emit("cancelled"); emit("cancelled");
dialog.value.close(); dialog.value!.close();
} }
function onLogin(res) { function onLogin(res: { id: string; i: string }) {
emit("done", res); emit("done", res);
dialog.value.close(); dialog.value!.close();
} }
</script> </script>

View file

@ -248,7 +248,7 @@
v-model="hCaptchaResponse" v-model="hCaptchaResponse"
class="_formBlock captcha" class="_formBlock captcha"
provider="hcaptcha" provider="hcaptcha"
:sitekey="instance.hcaptchaSiteKey" :sitekey="instance.hcaptchaSiteKey!"
/> />
<MkCaptcha <MkCaptcha
v-if="instance.enableRecaptcha" v-if="instance.enableRecaptcha"
@ -256,7 +256,7 @@
v-model="reCaptchaResponse" v-model="reCaptchaResponse"
class="_formBlock captcha" class="_formBlock captcha"
provider="recaptcha" provider="recaptcha"
:sitekey="instance.recaptchaSiteKey" :sitekey="instance.recaptchaSiteKey!"
/> />
<MkButton <MkButton
class="_formBlock" class="_formBlock"
@ -296,7 +296,7 @@ const props = withDefaults(
); );
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "signup", user: Record<string, any>): void; (ev: "signup", user: { id: string; i: string }): void;
(ev: "signupEmailPending"): void; (ev: "signupEmailPending"): void;
}>(); }>();

View file

@ -36,13 +36,13 @@ withDefaults(
); );
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "done"): void; (ev: "done", res: { id: string; i: string }): void;
(ev: "closed"): void; (ev: "closed"): void;
}>(); }>();
const dialog = ref<InstanceType<typeof XModalWindow>>(); const dialog = ref<InstanceType<typeof XModalWindow>>();
function onSignup(res) { function onSignup(res: { id: string; i: string }) {
emit("done", res); emit("done", res);
dialog.value?.close(); dialog.value?.close();
} }

View file

@ -2,8 +2,8 @@
<MkModal <MkModal
ref="modal" ref="modal"
:z-priority="'middle'" :z-priority="'middle'"
@click="$refs.modal.close()" @click="modal!.close()"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<div :class="$style.root"> <div :class="$style.root">
<div :class="$style.title"> <div :class="$style.title">
@ -14,7 +14,7 @@
:class="$style.gotIt" :class="$style.gotIt"
primary primary
full full
@click="$refs.modal.close()" @click="modal!.close()"
>{{ i18n.ts.gotIt }}</MkButton >{{ i18n.ts.gotIt }}</MkButton
> >
</div> </div>

View file

@ -1,6 +1,7 @@
import { markRaw } from "vue"; import { markRaw } from "vue";
import { locale } from "@/config"; import { locale } from "@/config";
// biome-ignore lint/suspicious/noExplicitAny: temporary use any
class I18n<T extends Record<string, any>> { class I18n<T extends Record<string, any>> {
public ts: T; public ts: T;

View file

@ -245,7 +245,12 @@ function checkForSplash() {
try { try {
// 変なバージョン文字列来るとcompareVersionsでエラーになるため // 変なバージョン文字列来ると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) { if (me) {
popup( popup(
@ -281,7 +286,7 @@ function checkForSplash() {
"closed", "closed",
); );
} else { } else {
unreadAnnouncements.forEach((item) => { for (const item of unreadAnnouncements) {
if (item.showPopup) if (item.showPopup)
popup( popup(
defineAsyncComponent( defineAsyncComponent(
@ -291,7 +296,7 @@ function checkForSplash() {
{}, {},
"closed", "closed",
); );
}); }
} }
}) })
.catch((err) => console.log(err)); .catch((err) => console.log(err));

View file

@ -1,7 +1,7 @@
// NIRAX --- A lightweight router // NIRAX --- A lightweight router
import { EventEmitter } from "eventemitter3"; import { EventEmitter } from "eventemitter3";
import type { Component, ShallowRef } from "vue"; import type { Component } from "vue";
import { shallowRef } from "vue"; import { shallowRef } from "vue";
import { safeURIDecode } from "@/scripts/safe-uri-decode"; import { safeURIDecode } from "@/scripts/safe-uri-decode";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
@ -36,6 +36,7 @@ export interface Resolved {
function parsePath(path: string): ParsedPath { function parsePath(path: string): ParsedPath {
const res = [] as ParsedPath; const res = [] as ParsedPath;
// biome-ignore lint/style/noParameterAssign: assign it intentionally
path = path.substring(1); path = path.substring(1);
for (const part of path.split("/")) { for (const part of path.split("/")) {
@ -76,13 +77,13 @@ export class Router extends EventEmitter<{
same: () => void; same: () => void;
}> { }> {
private routes: RouteDef[]; private routes: RouteDef[];
public current: Resolved; public current!: Resolved; // It is assigned in this.navigate
public currentRef: ShallowRef<Resolved> = shallowRef(); public currentRef = shallowRef<Resolved>();
public currentRoute: ShallowRef<RouteDef> = shallowRef(); public currentRoute = shallowRef<RouteDef>();
private currentPath: string; private currentPath: string;
private currentKey = Date.now().toString(); 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"]) { constructor(routes: Router["routes"], currentPath: Router["currentPath"]) {
super(); super();
@ -92,9 +93,10 @@ export class Router extends EventEmitter<{
this.navigate(currentPath, null, false); this.navigate(currentPath, null, false);
} }
public resolve(path: string): Resolved | null { public resolve(_path: string): Resolved | null {
let queryString: string | null = null; let queryString: string | null = null;
let hash: string | null = null; let hash: string | null = null;
let path = _path;
if (path[0] === "/") path = path.substring(1); if (path[0] === "/") path = path.substring(1);
if (path.includes("#")) { if (path.includes("#")) {
hash = path.substring(path.indexOf("#") + 1); hash = path.substring(path.indexOf("#") + 1);
@ -168,9 +170,16 @@ export class Router extends EventEmitter<{
} }
if (route.query != null && queryString != null) { if (route.query != null && queryString != null) {
const queryObject = [ // const queryObject = [
...new URLSearchParams(queryString).entries(), // ...new URLSearchParams(queryString).entries(),
].reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {}); // ].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) { for (const q in route.query) {
const as = route.query[q]; const as = route.query[q];
@ -227,6 +236,7 @@ export class Router extends EventEmitter<{
} }
const isSamePath = beforePath === path; const isSamePath = beforePath === path;
// biome-ignore lint/style/noParameterAssign: assign it intentionally
if (isSamePath && key == null) key = this.currentKey; if (isSamePath && key == null) key = this.currentKey;
this.current = res; this.current = res;
this.currentRef.value = res; this.currentRef.value = res;
@ -253,7 +263,7 @@ export class Router extends EventEmitter<{
return this.currentKey; return this.currentKey;
} }
public push(path: string, flag?: any) { public push(path: string, flag?: unknown) {
const beforePath = this.currentPath; const beforePath = this.currentPath;
if (path === beforePath) { if (path === beforePath) {
this.emit("same"); this.emit("same");

View file

@ -896,9 +896,6 @@ export async function openEmojiPicker(
...opts, ...opts,
}, },
{ {
chosen: (emoji) => {
insertTextAtCursor(activeTextarea, emoji);
},
done: (emoji) => { done: (emoji) => {
insertTextAtCursor(activeTextarea, emoji); insertTextAtCursor(activeTextarea, emoji);
}, },

View file

@ -7,7 +7,7 @@
i18n.ts._accountDelete.sendEmail i18n.ts._accountDelete.sendEmail
}}</FormInfo> }}</FormInfo>
<FormButton <FormButton
v-if="!me?.isDeleted" v-if="!me!.isDeleted"
danger danger
class="_formBlock" class="_formBlock"
@click="deleteAccount" @click="deleteAccount"

View file

@ -5,12 +5,13 @@ import { onUnmounted, ref, watch } from "vue";
import { api } from "./os"; import { api } from "./os";
import { useStream } from "./stream"; import { useStream } from "./stream";
import { isSignedIn, me } from "@/me"; import { isSignedIn, me } from "@/me";
import type { TypeUtils } from "firefish-js";
type StateDef = Record< type StateDef = Record<
string, string,
{ {
where: "account" | "device" | "deviceAccount"; 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)) { for (const [k, v] of Object.entries(state)) {
reactiveState[k] = ref(v); reactiveState[k] = ref(v);
} }
this.state = state as any; this.state = state as typeof this.state;
this.reactiveState = reactiveState as any; this.reactiveState = reactiveState as typeof this.reactiveState;
if (isSignedIn(me)) { if (isSignedIn(me)) {
// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう) // なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
// For some reason, if I don't setTimeout, an error occurs in the api function (probably caused by circular references)
window.setTimeout(() => { window.setTimeout(() => {
api("i/registry/get-all", { scope: ["client", this.key] }).then( api("i/registry/get-all", { scope: ["client", this.key] }).then(
(kvs) => { (kvs) => {
@ -104,7 +106,7 @@ export class Storage<T extends StateDef> {
} }
} }
localStorage.setItem( localStorage.setItem(
`${this.keyForLocalStorage}::cache::${me.id}`, `${this.keyForLocalStorage}::cache::${me!.id}`,
JSON.stringify(cache), JSON.stringify(cache),
); );
}, },
@ -118,11 +120,12 @@ export class Storage<T extends StateDef> {
key, key,
value, value,
}: { }: {
scope: string[]; scope?: string[];
key: keyof T; key: keyof T;
value: T[typeof key]["default"]; value: T[typeof key]["default"];
}) => { }) => {
if ( if (
scope == null ||
scope.length !== 2 || scope.length !== 2 ||
scope[0] !== "client" || scope[0] !== "client" ||
scope[1] !== this.key || scope[1] !== this.key ||
@ -135,13 +138,13 @@ export class Storage<T extends StateDef> {
const cache = JSON.parse( const cache = JSON.parse(
localStorage.getItem( localStorage.getItem(
`${this.keyForLocalStorage}::cache::${me.id}`, `${this.keyForLocalStorage}::cache::${me!.id}`,
) || "{}", ) || "{}",
); );
if (cache[key] !== value) { if (cache[key] !== value) {
cache[key] = value; cache[key] = value;
localStorage.setItem( localStorage.setItem(
`${this.keyForLocalStorage}::cache::${me.id}`, `${this.keyForLocalStorage}::cache::${me!.id}`,
JSON.stringify(cache), 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); if (_DEV_) console.log("set", key, value);
this.state[key] = value; this.state[key] = value;
@ -201,15 +204,15 @@ export class Storage<T extends StateDef> {
} }
} }
public push<K extends keyof T>( public push<K extends TypeUtils.PropertyOfType<T, { default: unknown[] }>>(
key: K, key: K & string,
value: ArrayElement<T[K]["default"]>, value: ArrayElement<T[K]["default"]>,
): void { ): void {
const currentState = this.state[key]; const currentState = this.state[key] as unknown[];
this.set(key, [...currentState, value]); this.set(key, [...currentState, value]);
} }
public reset(key: keyof T) { public reset(key: keyof T & string) {
this.set(key, this.def[key].default); this.set(key, this.def[key].default);
} }
@ -218,11 +221,11 @@ export class Storage<T extends StateDef> {
* vue場で設定コントロールのmodelとして使う用 * vue場で設定コントロールのmodelとして使う用
*/ */
public makeGetterSetter<K extends keyof T>( public makeGetterSetter<K extends keyof T>(
key: K, key: K & string,
getter?: (v: T[K]) => unknown, getter?: (oldV: T[K]["default"]) => T[K]["default"],
setter?: (v: unknown) => T[K], 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) => { const stop = watch(this.reactiveState[key], (val) => {
valueRef.value = val; valueRef.value = val;
@ -242,7 +245,7 @@ export class Storage<T extends StateDef> {
return valueRef.value; return valueRef.value;
} }
}, },
set: (value: unknown) => { set: (value: T[K]["default"]) => {
const val = setter ? setter(value) : value; const val = setter ? setter(value) : value;
this.set(key, val); this.set(key, val);
valueRef.value = val; valueRef.value = val;

View file

@ -42,7 +42,20 @@ export function install(plugin) {
aiscript.exec(parser.parse(plugin.src)); 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>(); const config = new Map<string, values.Value>();
for (const [k, v] of Object.entries(opts.plugin.config ?? {})) { for (const [k, v] of Object.entries(opts.plugin.config ?? {})) {
config.set( config.set(
@ -172,7 +185,7 @@ function registerNoteAction({ pluginId, title, handler }) {
if (!pluginContext) { if (!pluginContext) {
return; 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 { function registerPageViewInterruptor({ pluginId, handler }): void {
pageViewInterruptors.push({ // pageViewInterruptors.push({
handler: async (page) => { // handler: async (page) => {
const pluginContext = pluginContexts.get(pluginId); // const pluginContext = pluginContexts.get(pluginId);
if (!pluginContext) { // if (!pluginContext) {
return; // return;
} // }
return utils.valToJs( // return utils.valToJs(
await pluginContext.execFn(handler, [utils.jsToVal(page)]), // await pluginContext.execFn(handler, [utils.jsToVal(page)]),
); // );
}, // },
}); // });
} }

View file

@ -9,7 +9,7 @@ export function byteify(string: string, encoding: "ascii" | "base64" | "hex") {
); );
case "hex": case "hex":
return new Uint8Array( 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)),
); );
} }
} }

View file

@ -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 * 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 * Returns the longest prefix of elements that satisfy the predicate
*/ */
export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] { export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
const ys = []; const ys: T[] = [];
for (const x of xs) { for (const x of xs) {
if (f(x)) { if (f(x)) {
ys.push(x); ys.push(x);

View file

@ -13,7 +13,7 @@ export class Autocomplete {
} | null; } | null;
private textarea: HTMLInputElement | HTMLTextAreaElement; private textarea: HTMLInputElement | HTMLTextAreaElement;
private currentType: string; private currentType?: string;
private textRef: Ref<string>; private textRef: Ref<string>;
private opening: boolean; private opening: boolean;
@ -69,7 +69,7 @@ export class Autocomplete {
* *
*/ */
private onInput() { private onInput() {
const caretPos = this.textarea.selectionStart; const caretPos = this.textarea.selectionStart!;
const text = this.text.substring(0, caretPos).split("\n").pop()!; const text = this.text.substring(0, caretPos).split("\n").pop()!;
const mentionIndex = text.lastIndexOf("@"); const mentionIndex = text.lastIndexOf("@");
@ -147,10 +147,10 @@ export class Autocomplete {
this.opening = true; this.opening = true;
this.currentType = type; this.currentType = type;
// #region サジェストを表示すべき位置を計算 // #region Calculate the position where suggestions should be displayed
const caretPosition = getCaretCoordinates( const caretPosition = getCaretCoordinates(
this.textarea, this.textarea,
this.textarea.selectionStart, this.textarea.selectionStart!,
); );
const rect = this.textarea.getBoundingClientRect(); const rect = this.textarea.getBoundingClientRect();
@ -216,7 +216,7 @@ export class Autocomplete {
private complete({ type, value }) { private complete({ type, value }) {
this.close(); this.close();
const caret = this.textarea.selectionStart; const caret = this.textarea.selectionStart!;
if (type === "user") { if (type === "user") {
const source = this.text; const source = this.text;

View file

@ -1,4 +1,5 @@
import type { entities } from "firefish-js"; import type { entities } from "firefish-js";
import { detectLanguage, languageContains } from "./language-utils";
export interface Muted { export interface Muted {
muted: boolean; muted: boolean;
@ -12,11 +13,12 @@ function checkLangMute(
note: entities.Note, note: entities.Note,
mutedLangs: Array<string | string[]>, mutedLangs: Array<string | string[]>,
): Muted { ): Muted {
const mutedLangList = new Set( const mutedLangList = mutedLangs.flat();
mutedLangs.reduce((arr, x) => [...arr, ...(Array.isArray(x) ? x : [x])]), const noteLang = note.lang ?? detectLanguage(note.text ?? "") ?? "no-lang";
); for (const mutedLang of mutedLangList) {
if (mutedLangList.has((note.lang?.[0]?.lang || "").split("-")[0])) { if (languageContains(mutedLang, noteLang)) {
return { muted: true, matched: [note.lang?.[0]?.lang] }; return { muted: true, matched: [noteLang] };
}
} }
return NotMuted; return NotMuted;
} }
@ -32,7 +34,7 @@ function checkWordMute(
if (text === "") return NotMuted; if (text === "") return NotMuted;
const result = { muted: false, matched: [] }; const result = { muted: false, matched: [] as string[] };
for (const mutePattern of mutedWords) { for (const mutePattern of mutedWords) {
if (Array.isArray(mutePattern)) { if (Array.isArray(mutePattern)) {
@ -74,7 +76,7 @@ function checkWordMute(
} }
export function getWordSoftMute( export function getWordSoftMute(
note: firefish.entities.Note, note: entities.Note,
meId: string | null | undefined, meId: string | null | undefined,
mutedWords: Array<string | string[]>, mutedWords: Array<string | string[]>,
mutedLangs: Array<string | string[]>, mutedLangs: Array<string | string[]>,

View file

@ -1,6 +1,8 @@
export function collectPageVars(content) { import type { PageContent, PageVar } from "@/types/page";
const pageVars = [];
const collect = (xs: any[]) => { export function collectPageVars(content: PageContent[]) {
const pageVars: PageVar[] = [];
const collect = (xs: PageContent[]) => {
for (const x of xs) { for (const x of xs) {
if (x.type === "textInput") { if (x.type === "textInput") {
pageVars.push({ pageVars.push({
@ -24,7 +26,7 @@ export function collectPageVars(content) {
pageVars.push({ pageVars.push({
name: x.name, name: x.name,
type: "boolean", type: "boolean",
value: x.default, value: x.default!,
}); });
} else if (x.type === "counter") { } else if (x.type === "counter") {
pageVars.push({ pageVars.push({

View file

@ -1,7 +1,7 @@
/** /**
* Clipboardに値をコピー(TODO: 文字列以外も対応) * Clipboardに値をコピー(TODO: 文字列以外も対応)
*/ */
export default (val) => { function obsoleteCopyToClipboard(val: string) {
// 空div 生成 // 空div 生成
const tmp = document.createElement("div"); const tmp = document.createElement("div");
// 選択用のタグ生成 // 選択用のタグ生成
@ -21,7 +21,7 @@ export default (val) => {
// body に追加 // body に追加
document.body.appendChild(tmp); document.body.appendChild(tmp);
// 要素を選択 // 要素を選択
document.getSelection().selectAllChildren(tmp); document.getSelection()?.selectAllChildren(tmp);
// クリップボードにコピー // クリップボードにコピー
const result = document.execCommand("copy"); const result = document.execCommand("copy");
@ -30,4 +30,20 @@ export default (val) => {
document.body.removeChild(tmp); document.body.removeChild(tmp);
return result; 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)));
});
}
}

View file

@ -6,7 +6,10 @@ export function extractMentions(
nodes: mfm.MfmNode[], nodes: mfm.MfmNode[],
): mfm.MfmMention["props"][] { ): mfm.MfmMention["props"][] {
// TODO: 重複を削除 // 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); const mentions = mentionNodes.map((x) => x.props);
return mentions; return mentions;

View file

@ -15,7 +15,8 @@ const animatedMfm = [
export function extractMfmWithAnimation(nodes: mfm.MfmNode[]): string[] { export function extractMfmWithAnimation(nodes: mfm.MfmNode[]): string[] {
const mfmNodes = mfm.extract(nodes, (node) => { const mfmNodes = mfm.extract(nodes, (node) => {
return node.type === "fn" && animatedMfm.includes(node.props.name); return node.type === "fn" && animatedMfm.includes(node.props.name);
}); }) as mfm.MfmFn[];
// FIXME: mfm type error
const mfms = mfmNodes.map((x) => x.props.fn); const mfms = mfmNodes.map((x) => x.props.fn);
return mfms; return mfms;

View file

@ -14,7 +14,7 @@ export function extractUrlFromMfm(
node.type === "url" || node.type === "url" ||
(node.type === "link" && !(respectSilentFlag && node.props.silent)) (node.type === "link" && !(respectSilentFlag && node.props.silent))
); );
}); }) as (mfm.MfmLink | mfm.MfmUrl)[];
const urls: string[] = unique(urlNodes.map((x) => x.props.url)); const urls: string[] = unique(urlNodes.map((x) => x.props.url));
return urls.reduce((array, url) => { return urls.reduce((array, url) => {

View file

@ -1,5 +1,6 @@
export function focusPrev(el: Element | null, self = false, scroll = true) { export function focusPrev(el: Element | null, self = false, scroll = true) {
if (el == null) return; if (el == null) return;
// biome-ignore lint/style/noParameterAssign: assign it intentionally
if (!self) el = el.previousElementSibling; if (!self) el = el.previousElementSibling;
if (el) { if (el) {
if (el.hasAttribute("tabindex")) { 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) { export function focusNext(el: Element | null, self = false, scroll = true) {
if (el == null) return; if (el == null) return;
// biome-ignore lint/style/noParameterAssign: assign it intentionally
if (!self) el = el.nextElementSibling; if (!self) el = el.nextElementSibling;
if (el) { if (el) {
if (el.hasAttribute("tabindex")) { if (el.hasAttribute("tabindex")) {

View file

@ -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,
};
}

View file

@ -15,6 +15,7 @@ import { useRouter } from "@/router";
import { notePage } from "@/filters/note"; import { notePage } from "@/filters/note";
import type { NoteTranslation } from "@/types/note"; import type { NoteTranslation } from "@/types/note";
import type { MenuItem } from "@/types/menu"; import type { MenuItem } from "@/types/menu";
import type { NoteDraft } from "@/types/post-form";
const router = useRouter(); const router = useRouter();
@ -72,7 +73,7 @@ export function getNoteMenu(props: {
}); });
os.post({ os.post({
initialNote: appearNote, initialNote: appearNote as NoteDraft,
renote: appearNote.renote, renote: appearNote.renote,
reply: appearNote.reply, reply: appearNote.reply,
channel: appearNote.channel, channel: appearNote.channel,
@ -83,7 +84,7 @@ export function getNoteMenu(props: {
async function edit() { async function edit() {
os.post({ os.post({
initialNote: appearNote, initialNote: appearNote as NoteDraft,
renote: appearNote.renote, renote: appearNote.renote,
reply: appearNote.reply, reply: appearNote.reply,
channel: appearNote.channel, channel: appearNote.channel,

View file

@ -1,385 +1,6 @@
// TODO: sharedに置いてバックエンドのと統合したい import { langmap as _langmap } from "firefish-js";
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",
},
};
export const iso639Langs3 = { export const langmap = _langmap;
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);
/** /**
* @see https://github.com/komodojp/tinyld/blob/develop/docs/langs.md * @see https://github.com/komodojp/tinyld/blob/develop/docs/langs.md

View file

@ -39,7 +39,7 @@ export function languageContains(
) { ) {
if (!langCode1 || !langCode2) return false; if (!langCode1 || !langCode2) return false;
return parentLanguage(langCode2) === langCode1; return langCode1 === langCode2 || parentLanguage(langCode2) === langCode1;
} }
export function parentLanguage(langCode: string | null) { export function parentLanguage(langCode: string | null) {

View file

@ -1,9 +1,12 @@
import { markRaw, ref } from "vue"; import { markRaw } from "vue";
import type { ApiTypes, entities } from "firefish-js"; import type { ApiTypes, entities } from "firefish-js";
import { isSignedIn, me } from "./me"; import { isSignedIn, me } from "./me";
import { Storage } from "./pizzax"; import { Storage } from "./pizzax";
import type { NoteVisibility } from "@/types/note"; import type { NoteVisibility } from "@/types/note";
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
type TODO = any;
export const postFormActions: { export const postFormActions: {
title: string; title: string;
handler: (from, update) => void | Promise<void>; handler: (from, update) => void | Promise<void>;
@ -152,7 +155,7 @@ export const defaultStore = markRaw(
type: string; type: string;
size: "verySmall" | "small" | "medium" | "large" | "veryLarge"; size: "verySmall" | "small" | "medium" | "large" | "veryLarge";
black: boolean; black: boolean;
props: Record<string, any>; props: Record<string, TODO>;
}[], }[],
}, },
widgets: { widgets: {
@ -161,7 +164,7 @@ export const defaultStore = markRaw(
name: string; name: string;
id: string; id: string;
place: string | null; place: string | null;
data: Record<string, any>; data: Record<string, TODO>;
}[], }[],
}, },
tl: { tl: {
@ -465,109 +468,6 @@ export const defaultStore = markRaw(
}), }),
); );
// TODO: 他のタブと永続化されたstateを同期 import ColdStore from "./cold-store";
const PREFIX = "miux:"; export const ColdDeviceStorage = ColdStore;
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);
},
};
}
}

View file

@ -31,6 +31,7 @@ export function reloadStream() {
isReloading = true; isReloading = true;
stream.close(); stream.close();
// biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally
stream.once("_connected_", () => (isReloading = false)); stream.once("_connected_", () => (isReloading = false));
stream.stream.reconnect(); stream.stream.reconnect();
isReloading = false; isReloading = false;

View file

@ -17,7 +17,8 @@ export async function fetchThemes(): Promise<void> {
key: "themes", key: "themes",
}); });
localStorage.setItem(lsCacheKey, JSON.stringify(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; if (err.code === "NO_SUCH_KEY") return;
throw err; throw err;
} }

View file

@ -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;
};

View file

@ -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>;

View file

@ -17,7 +17,7 @@
}, },
"minify": false, "minify": false,
"module": { "module": {
"type": "commonjs", "type": "es6",
"strict": true, "strict": true,
"resolveFully": true "resolveFully": true
} }

View file

@ -4,6 +4,7 @@
"description": "Firefish SDK for JavaScript", "description": "Firefish SDK for JavaScript",
"homepage": "https://firefish.dev/firefish/firefish/-/tree/develop/packages/firefish-js", "homepage": "https://firefish.dev/firefish/firefish/-/tree/develop/packages/firefish-js",
"main": "./built/index.js", "main": "./built/index.js",
"type": "module",
"types": "./src/index.ts", "types": "./src/index.ts",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@ -35,7 +36,7 @@
"typescript": "5.4.5" "typescript": "5.4.5"
}, },
"files": [ "files": [
"built" "built", "src"
], ],
"dependencies": { "dependencies": {
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",

View file

@ -1,4 +1,4 @@
import type { Endpoints } from "./api.types"; import type { Endpoints } from "./api.types.js";
const MK_API_ERROR = Symbol(); const MK_API_ERROR = Symbol();
@ -7,10 +7,12 @@ export type APIError = {
code: string; code: string;
message: string; message: string;
kind: "client" | "server"; 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; return reason[MK_API_ERROR] === true;
} }
@ -24,7 +26,7 @@ export type FetchLike = (
}, },
) => Promise<{ ) => Promise<{
status: number; status: number;
json(): Promise<any>; json(): Promise<ExplicitlyUsedAny>;
}>; }>;
type IsNeverType<T> = [T] extends [never] ? true : false; type IsNeverType<T> = [T] extends [never] ? true : false;
@ -36,7 +38,10 @@ type IsCaseMatched<
P extends Endpoints[E]["req"], P extends Endpoints[E]["req"],
C extends number, C extends number,
> = IsNeverType< > = IsNeverType<
StrictExtract<Endpoints[E]["res"]["$switch"]["$cases"][C], [P, any]> StrictExtract<
Endpoints[E]["res"]["$switch"]["$cases"][C],
[P, ExplicitlyUsedAny]
>
> extends false > extends false
? true ? true
: false; : false;
@ -45,7 +50,10 @@ type GetCaseResult<
E extends keyof Endpoints, E extends keyof Endpoints,
P extends Endpoints[E]["req"], P extends Endpoints[E]["req"],
C extends number, 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 { export class APIClient {
public origin: string; public origin: string;
@ -70,7 +78,7 @@ export class APIClient {
credential?: string | null | undefined, credential?: string | null | undefined,
): Promise< ): Promise<
Endpoints[E]["res"] extends { Endpoints[E]["res"] extends {
$switch: { $cases: [any, any][]; $default: any }; $switch: { $cases: [unknown, unknown][]; $default: unknown };
} }
? IsCaseMatched<E, P, 0> extends true ? IsCaseMatched<E, P, 0> extends true
? GetCaseResult<E, P, 0> ? GetCaseResult<E, P, 0>

View file

@ -38,7 +38,7 @@ import type {
UserList, UserList,
UserLite, UserLite,
UserSorting, UserSorting,
} from "./entities"; } from "./entities.js";
import type * as consts from "./consts"; import type * as consts from "./consts";
@ -264,9 +264,9 @@ export type Endpoints = {
// clips // clips
"clips/add-note": { req: TODO; res: TODO }; "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/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/notes": { req: TODO; res: TODO };
"clips/show": { req: TODO; res: TODO }; "clips/show": { req: TODO; res: TODO };
"clips/update": { req: TODO; res: TODO }; "clips/update": { req: TODO; res: TODO };
@ -363,6 +363,16 @@ export type Endpoints = {
res: DriveFile[]; res: DriveFile[];
}; };
"email-address/available": {
req: {
emailAddress: string;
};
res: {
available?: boolean;
reason: string | null;
};
};
// endpoint // endpoint
endpoint: { endpoint: {
req: { endpoint: string }; req: { endpoint: string };
@ -739,6 +749,18 @@ export type Endpoints = {
}; };
res: Note[]; 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": { "notes/hybrid-timeline": {
req: { req: {
limit?: number; limit?: number;
@ -759,6 +781,12 @@ export type Endpoints = {
}; };
res: Note[]; res: Note[];
}; };
"notes/make-private": {
req: {
noteId: Note["id"];
};
res: null;
};
"notes/mentions": { "notes/mentions": {
req: { req: {
following?: boolean; following?: boolean;
@ -900,6 +928,16 @@ export type Endpoints = {
// promo // promo
"promo/read": { req: TODO; res: TODO }; "promo/read": { req: TODO; res: TODO };
// release
release: {
req: null;
res: {
version: string;
notes: string;
screenshots: string[];
};
};
// request-reset-password // request-reset-password
"request-reset-password": { "request-reset-password": {
req: { username: string; email: string }; req: { username: string; email: string };
@ -922,8 +960,36 @@ export type Endpoints = {
// ck specific // ck specific
"latest-version": { req: NoParams; res: TODO }; "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
"sw/register": { req: TODO; res: TODO }; "sw/register": { req: TODO; res: TODO };
"sw/unregister": {
req: {
endpoint: string;
};
res: null;
};
// username // username
"username/available": { "username/available": {

View file

@ -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 ID = string;
export type DateString = string; export type DateString = string;
@ -116,6 +117,7 @@ export type MeDetailed = UserDetailed & {
preventAiLearning: boolean; preventAiLearning: boolean;
receiveAnnouncementEmail: boolean; receiveAnnouncementEmail: boolean;
usePasswordLessLogin: boolean; usePasswordLessLogin: boolean;
token: string;
[other: string]: any; [other: string]: any;
}; };
@ -482,7 +484,7 @@ export type Announcement = {
imageUrl: string | null; imageUrl: string | null;
isRead?: boolean; isRead?: boolean;
isGoodNews: boolean; isGoodNews: boolean;
showPopUp: boolean; showPopup: boolean;
}; };
export type Antenna = { export type Antenna = {
@ -513,7 +515,7 @@ export type AuthSession = {
export type Ad = TODO; export type Ad = TODO;
export type Clip = TODO; export type Clip = Packed<"Clip">;
export type NoteFavorite = { export type NoteFavorite = {
id: ID; id: ID;

View file

@ -1,14 +1,17 @@
import * as acct from "./acct"; import * as acct from "./acct.js";
import type { Acct } from "./acct"; import type { Acct } from "./acct.js";
import { Endpoints } from "./api.types"; import type { Endpoints } from "./api.types.js";
import type * as ApiTypes from "./api.types"; import type * as ApiTypes from "./api.types.js";
import * as consts from "./consts"; import * as consts from "./consts.js";
import Stream, { Connection } from "./streaming"; import Stream, { Connection } from "./streaming.js";
import * as StreamTypes from "./streaming.types"; import * as StreamTypes from "./streaming.types.js";
import type * as TypeUtils from "./type-utils"; import type * as TypeUtils from "./type-utils.js";
import type * as SchemaTypes from "./misc/schema.js";
import * as Schema from "./misc/schema.js";
export { export {
Endpoints, type Endpoints,
type ApiTypes, type ApiTypes,
Stream, Stream,
Connection as ChannelConnection, Connection as ChannelConnection,
@ -16,6 +19,8 @@ export {
acct, acct,
type Acct, type Acct,
type TypeUtils, type TypeUtils,
Schema,
type SchemaTypes,
}; };
export const permissions = consts.permissions; export const permissions = consts.permissions;
@ -26,9 +31,12 @@ export const languages = consts.languages;
export const ffVisibility = consts.ffVisibility; export const ffVisibility = consts.ffVisibility;
export const instanceSortParam = consts.instanceSortParam; export const instanceSortParam = consts.instanceSortParam;
import { langmap, type PostLanguage } from "./misc/langmap.js";
export { langmap, type PostLanguage };
// api extractor not supported yet // api extractor not supported yet
//export * as api from './api'; //export * as api from './api';
//export * as entities from './entities'; //export * as entities from './entities';
import * as api from "./api"; import * as api from "./api.js";
import * as entities from "./entities"; import * as entities from "./entities.js";
export { api, entities }; export { api, entities };

View file

@ -1,4 +1,3 @@
// TODO: sharedに置いてバックエンドのと統合したい
export const iso639Langs1 = { export const iso639Langs1 = {
af: { af: {
nativeName: "Afrikaans", nativeName: "Afrikaans",

View file

@ -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>>;

View file

@ -1,5 +1,3 @@
import { config } from "@/config.js";
export const packedFederationInstanceSchema = { export const packedFederationInstanceSchema = {
type: "object", type: "object",
properties: { properties: {
@ -83,7 +81,7 @@ export const packedFederationInstanceSchema = {
type: "string", type: "string",
optional: false, optional: false,
nullable: true, nullable: true,
example: config.version, example: "20240424",
}, },
openRegistrations: { openRegistrations: {
type: "boolean", type: "boolean",

View file

@ -1,4 +1,4 @@
import { langmap } from "@/misc/langmap.js"; import { langmap } from "../misc/langmap.js";
export const packedNoteSchema = { export const packedNoteSchema = {
type: "object", type: "object",
@ -208,15 +208,5 @@ export const packedNoteSchema = {
optional: true, optional: true,
nullable: true, nullable: true,
}, },
myRenoteCount: {
type: "number",
optional: true,
nullable: false,
},
quoteCount: {
type: "number",
optional: false,
nullable: false,
},
}, },
} as const; } as const;

View file

@ -1,4 +1,4 @@
import { notificationTypes } from "@/types.js"; import { notificationTypes } from "../consts.js";
export const packedNotificationSchema = { export const packedNotificationSchema = {
type: "object", type: "object",

View file

@ -1,6 +1,6 @@
import { EventEmitter } from "eventemitter3"; import { EventEmitter } from "eventemitter3";
import ReconnectingWebsocket from "reconnecting"; import ReconnectingWebsocket from "reconnecting";
import type { BroadcastEvents, Channels } from "./streaming.types"; import type { BroadcastEvents, Channels } from "./streaming.types.js";
function autobind(instance: any): void { function autobind(instance: any): void {
const prototype = Object.getPrototypeOf(instance); const prototype = Object.getPrototypeOf(instance);

View file

@ -12,7 +12,7 @@ import type {
UserGroup, UserGroup,
UserLite, UserLite,
} from "./entities"; } from "./entities";
import type { Connection } from "./streaming"; import type { Connection } from "./streaming.js";
type FIXME = any; type FIXME = any;

View file

@ -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; [K in keyof Type]: Type[K] extends U ? K : never;
}[keyof Type]; }[keyof Type];
export type EndpointsOf<T> = PropertyOfType<Endpoints, { res: T }>; export type EndpointsOf<T> = PropertyOfType<Endpoints, { res: T }>;
export type NonUndefinedAble<T> = T extends undefined ? never : T;

View file

@ -141,6 +141,9 @@ importers:
file-type: file-type:
specifier: 19.0.0 specifier: 19.0.0
version: 19.0.0 version: 19.0.0
firefish-js:
specifier: workspace:*
version: link:../firefish-js
fluent-ffmpeg: fluent-ffmpeg:
specifier: 2.1.2 specifier: 2.1.2
version: 2.1.2 version: 2.1.2