diff options
| author | Marie <marie@kaifa.ch> | 2024-01-09 02:57:57 +0100 |
|---|---|---|
| committer | Marie <marie@kaifa.ch> | 2024-01-09 02:57:57 +0100 |
| commit | 7552cea69ae21b31799d54b246dcd45e96654926 (patch) | |
| tree | 389031f25fee72180157869a9e60c31704e198a9 | |
| parent | merge: additional authorised fetch logging (#328) (diff) | |
| parent | feat(ci): api.jsonのバリデーションチェックCIを追加 (#12950) (diff) | |
| download | sharkey-7552cea69ae21b31799d54b246dcd45e96654926.tar.gz sharkey-7552cea69ae21b31799d54b246dcd45e96654926.tar.bz2 sharkey-7552cea69ae21b31799d54b246dcd45e96654926.zip | |
merge: upstream
413 files changed, 5516 insertions, 2308 deletions
diff --git a/.config/docker_example.env b/.config/docker_example.env index 7a0261524b..4fe8e76b78 100644 --- a/.config/docker_example.env +++ b/.config/docker_example.env @@ -2,3 +2,4 @@ POSTGRES_PASSWORD=example-misskey-pass POSTGRES_USER=example-misskey-user POSTGRES_DB=misskey +DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" diff --git a/.forgejo/workflows/lint.yml b/.forgejo/workflows/lint.yml index 0a773d5fb0..e77c3e7a77 100644 --- a/.forgejo/workflows/lint.yml +++ b/.forgejo/workflows/lint.yml @@ -8,6 +8,12 @@ on: paths: - packages/** pull_request: + paths: + - packages/backend/** + - packages/frontend/** + - packages/sw/** + - packages/misskey-js/** + - packages/shared/.eslintrc.js jobs: pnpm_install: diff --git a/.gitea/ISSUE_TEMPLATE/02_feature-request.yml b/.gitea/ISSUE_TEMPLATE/02_feature-request.yml index d3bf64d869..b9f6d09aa0 100644 --- a/.gitea/ISSUE_TEMPLATE/02_feature-request.yml +++ b/.gitea/ISSUE_TEMPLATE/02_feature-request.yml @@ -19,4 +19,4 @@ body: attributes: label: Do you want to implement this feature yourself? options: - - label: Yes, I will implement this by myself and send a pull request
\ No newline at end of file + - label: Yes, I will implement this by myself and send a pull request diff --git a/.gitignore b/.gitignore index 11e69b2621..216b5548ea 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ docker-compose.yml # misskey /build built +built-test /data /.cache-loader /db diff --git a/CHANGELOG.md b/CHANGELOG.md index 15845124b8..2270bb1c13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,22 @@ -<!-- -## 2023.x.x (unreleased) +## 202x.x.x (Unreleased) ### General -- +- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加 +- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正 ### Client -- Enhance: Adjusted styling to be closer to Firefish +- Feat: 新しいゲームを追加 +- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように +- Enhance: チャンネルノートのピン留めをノートのメニューからできるように +- Fix: ネイティブモードの絵文字がモノクロにならないように +- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 +- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正 ### Server -- - ---> +- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました +- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916) +- Enhance: クリップをエクスポートできるように +- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正 ## 2023.12.2 @@ -1,5 +1,5 @@ Unless otherwise stated this repository is -Copyright © 2014-2023 syuilo and contributers +Copyright © 2014-2024 syuilo and contributors And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE. diff --git a/ROADMAP.md b/ROADMAP.md index 3077c41e73..509ecb9fe7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,6 +6,7 @@ Also, the later tasks are more indefinite and are subject to change as developme This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development. - ~~Make the number of type errors zero (backend)~~ → Done ✔️ +- Make the number of type errors zero (frontend) - Improve CI - ~~Fix tests~~ → Done ✔️ - Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986 diff --git a/docker-compose_example.yml b/docker-compose_example.yml index 0a422a2a91..6e291b4596 100644 --- a/docker-compose_example.yml +++ b/docker-compose_example.yml @@ -8,6 +8,7 @@ services: links: - db - redis +# - mcaptcha # - meilisearch depends_on: db: @@ -48,6 +49,36 @@ services: interval: 5s retries: 20 +# mcaptcha: +# restart: always +# image: mcaptcha/mcaptcha:latest +# networks: +# internal_network: +# external_network: +# aliases: +# - localhost +# ports: +# - 7493:7493 +# env_file: +# - .config/docker.env +# environment: +# PORT: 7493 +# MCAPTCHA_redis_URL: "redis://mcaptcha_redis/" +# depends_on: +# db: +# condition: service_healthy +# mcaptcha_redis: +# condition: service_healthy +# +# mcaptcha_redis: +# image: mcaptcha/cache:latest +# networks: +# - internal_network +# healthcheck: +# test: "redis-cli ping" +# interval: 5s +# retries: 20 + # meilisearch: # restart: always # image: getmeili/meilisearch:v1.3.4 diff --git a/locales/en-US.yml b/locales/en-US.yml index 39c808ae50..0affe133ed 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -127,7 +127,7 @@ reaction: "Reactions" reactions: "Reactions" emojiPicker: "Emoji picker" pinnedEmojisForReactionSettingDescription: "Set the emojis which should be pinned and displayed immediately when reacting." -pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when entering emojis" +pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker" emojiPickerDisplay: "Emoji picker display" overwriteFromPinnedEmojisForReaction: "Override from reaction settings" overwriteFromPinnedEmojis: "Override from general settings" diff --git a/locales/index.d.ts b/locales/index.d.ts index ad002cd4f5..9415661000 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -393,6 +393,11 @@ export interface Locale { "enableHcaptcha": string; "hcaptchaSiteKey": string; "hcaptchaSecretKey": string; + "mcaptcha": string; + "enableMcaptcha": string; + "mcaptchaSiteKey": string; + "mcaptchaSecretKey": string; + "mcaptchaInstanceUrl": string; "recaptcha": string; "enableRecaptcha": string; "recaptchaSiteKey": string; @@ -686,6 +691,7 @@ export interface Locale { "other": string; "regenerateLoginToken": string; "regenerateLoginTokenDescription": string; + "theKeywordWhenSearchingForCustomEmoji": string; "setMultipleBySeparatingWithSpace": string; "fileIdOrUrl": string; "behavior": string; @@ -1226,6 +1232,7 @@ export interface Locale { "decorate": string; "addMfmFunction": string; "enableQuickAddMfmFunction": string; + "bubbleGame": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; @@ -1690,6 +1697,15 @@ export interface Locale { "title": string; "description": string; }; + "_bubbleGameExplodingHead": { + "title": string; + "description": string; + }; + "_bubbleGameDoubleExplodingHead": { + "title": string; + "description": string; + "flavor": string; + }; }; }; "_role": { @@ -2302,6 +2318,7 @@ export interface Locale { "_exportOrImport": { "allNotes": string; "favoritedNotes": string; + "clips": string; "followingList": string; "muteList": string; "blockingList": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c2f8b9e499..44430bc0af 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -390,6 +390,11 @@ hcaptcha: "hCaptcha" enableHcaptcha: "hCaptchaを有効にする" hcaptchaSiteKey: "サイトキー" hcaptchaSecretKey: "シークレットキー" +mcaptcha: "mCaptcha" +enableMcaptcha: "mCaptchaを有効にする" +mcaptchaSiteKey: "サイトキー" +mcaptchaSecretKey: "シークレットキー" +mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHAを有効にする" recaptchaSiteKey: "サイトキー" @@ -683,6 +688,7 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使 other: "その他" regenerateLoginToken: "ログイントークンを再生成" regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。" +theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。" setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" fileIdOrUrl: "ファイルIDまたはURL" behavior: "動作" @@ -1223,6 +1229,7 @@ seasonalScreenEffect: "季節に応じた画面の演出" decorate: "デコる" addMfmFunction: "装飾を追加" enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" +bubbleGame: "バブルゲーム" _announcement: forExistingUsers: "既存ユーザーのみ" @@ -1601,6 +1608,13 @@ _achievements: _tutorialCompleted: title: "Sharkey初心者講座 修了証" description: "チュートリアルを完了した" + _bubbleGameExplodingHead: + title: "🤯" + description: "バブルゲームで最も大きいモノを出した" + _bubbleGameDoubleExplodingHead: + title: "ダブル🤯" + description: "バブルゲームで最も大きいモノを2つ同時に出した" + flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて" _role: new: "ロールの作成" @@ -2205,6 +2219,7 @@ _profile: _exportOrImport: allNotes: "全てのノート" favoritedNotes: "お気に入りにしたノート" + clips: "クリップ" followingList: "フォロー" muteList: "ミュート" blockingList: "ブロック" diff --git a/package.json b/package.json index 91e6e42d11..6ed8726cc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2024.1.0.beta1", + "version": "2024.1.0.beta2", "codename": "shonk", "repository": { "type": "git", diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index 97d777c862..5a4aa4e15a 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -160,7 +160,6 @@ module.exports = { testMatch: [ "<rootDir>/test/unit/**/*.ts", "<rootDir>/src/**/*.test.ts", - "<rootDir>/test/e2e/**/*.ts", ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped diff --git a/packages/backend/jest.config.e2e.cjs b/packages/backend/jest.config.e2e.cjs new file mode 100644 index 0000000000..4502da47df --- /dev/null +++ b/packages/backend/jest.config.e2e.cjs @@ -0,0 +1,15 @@ +/* +* For a detailed explanation regarding each configuration property and type check, visit: +* https://jestjs.io/docs/en/configuration.html +*/ + +const base = require('./jest.config.cjs') + +module.exports = { + ...base, + globalSetup: "<rootDir>/built-test/entry.js", + setupFilesAfterEnv: ["<rootDir>/test/jest.setup.ts"], + testMatch: [ + "<rootDir>/test/e2e/**/*.ts", + ], +}; diff --git a/packages/backend/jest.config.unit.cjs b/packages/backend/jest.config.unit.cjs new file mode 100644 index 0000000000..aa5992936b --- /dev/null +++ b/packages/backend/jest.config.unit.cjs @@ -0,0 +1,14 @@ +/* +* For a detailed explanation regarding each configuration property and type check, visit: +* https://jestjs.io/docs/en/configuration.html +*/ + +const base = require('./jest.config.cjs') + +module.exports = { + ...base, + testMatch: [ + "<rootDir>/test/unit/**/*.ts", + "<rootDir>/src/**/*.test.ts", + ], +}; diff --git a/packages/backend/migration/1703658526000-supportTrueMailApi.js b/packages/backend/migration/1703658526000-supportTrueMailApi.js new file mode 100644 index 0000000000..0054d54122 --- /dev/null +++ b/packages/backend/migration/1703658526000-supportTrueMailApi.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SupportTrueMailApi1703658526000 { + name = 'SupportTrueMailApi1703658526000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "truemailInstance" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "truemailAuthKey" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "enableTruemailApi" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTruemailApi"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "truemailInstance"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "truemailAuthKey"`); + } +} diff --git a/packages/backend/migration/1704373210054-support-mcaptcha.js b/packages/backend/migration/1704373210054-support-mcaptcha.js new file mode 100644 index 0000000000..ce42b90716 --- /dev/null +++ b/packages/backend/migration/1704373210054-support-mcaptcha.js @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SupportMcaptcha1704373210054 { + name = 'SupportMcaptcha1704373210054' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableMcaptcha" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSitekey" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSecretKey" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaInstanceUrl" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaInstanceUrl"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSecretKey"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSitekey"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableMcaptcha"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 2aa10b1c96..05dbdd71fe 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -13,6 +13,7 @@ "revert": "pnpm typeorm migration:revert -d ormconfig.js", "check:connect": "node ./check_connect.js", "build": "swc src -d built -D", + "build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc", "watch:swc": "swc src -d built -D -w", "build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", "watch": "node watch.mjs", @@ -21,11 +22,15 @@ "typecheck": "pnpm --filter megalodon build && tsc --noEmit", "eslint": "eslint --quiet \"src/**/*.ts\"", "lint": "pnpm typecheck && pnpm eslint", - "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit", - "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit", + "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs", + "jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs", + "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs", + "jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs", "jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache", "test": "pnpm jest", + "test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e", "test-and-coverage": "pnpm jest-and-coverage", + "test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e", "generate-api-json": "node ./generate_api_json.js" }, "optionalDependencies": { @@ -72,6 +77,8 @@ "@fastify/multipart": "8.0.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", + "@misskey-dev/sharp-read-bmp": "^1.1.1", + "@misskey-dev/summaly": "^5.0.3", "@nestjs/common": "10.2.10", "@nestjs/core": "10.2.10", "@nestjs/testing": "10.2.10", @@ -158,11 +165,9 @@ "sanitize-html": "2.11.0", "secure-json-parse": "2.7.0", "sharp": "0.32.6", - "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "summaly": "github:misskey-dev/summaly", "systeminformation": "5.21.20", "tinycolor2": "1.6.0", "tmp": "0.2.1", @@ -179,6 +184,8 @@ }, "devDependencies": { "@jest/globals": "29.7.0", + "@misskey-dev/eslint-plugin": "^1.0.0", + "@nestjs/platform-express": "^10.3.0", "@simplewebauthn/typescript-types": "8.3.4", "@swc/jest": "0.2.29", "@types/accepts": "1.3.7", @@ -228,9 +235,11 @@ "eslint": "8.56.0", "eslint-plugin-import": "2.29.1", "execa": "8.0.1", + "fkill": "^9.0.0", "jest": "29.7.0", "jest-mock": "29.7.0", "nodemon": "3.0.2", + "pid-port": "^1.0.0", "simple-oauth2": "5.0.0" } } diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 3e9d19f825..c83845b94c 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { setTimeout } from 'node:timers/promises'; import { Global, Inject, Module } from '@nestjs/common'; import * as Redis from 'ioredis'; import { DataSource } from 'typeorm'; @@ -12,6 +11,7 @@ import { DI } from './di-symbols.js'; import { Config, loadConfig } from './config.js'; import { createPostgresDataSource } from './postgres.js'; import { RepositoryModule } from './models/RepositoryModule.js'; +import { allSettled } from './misc/promise-tracker.js'; import type { Provider, OnApplicationShutdown } from '@nestjs/common'; const $config: Provider = { @@ -33,7 +33,7 @@ const $meilisearch: Provider = { useFactory: (config: Config) => { if (config.meilisearch) { return new MeiliSearch({ - host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`, + host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`, apiKey: config.meilisearch.apiKey, }); } else { @@ -91,17 +91,12 @@ export class GlobalModule implements OnApplicationShutdown { @Inject(DI.redisForPub) private redisForPub: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, - ) {} + ) { } public async dispose(): Promise<void> { - if (process.env.NODE_ENV === 'test') { - // XXX: - // Shutting down the existing connections causes errors on Jest as - // Misskey has asynchronous postgres/redis connections that are not - // awaited. - // Let's wait for some random time for them to finish. - await setTimeout(5000); - } + // Wait for all potential DB queries + await allSettled(); + // And then disconnect from DB await Promise.all([ this.db.destroy(), this.redisClient.disconnect(), diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts index 88fc033859..a28b68ee86 100644 --- a/packages/backend/src/core/AchievementService.ts +++ b/packages/backend/src/core/AchievementService.ts @@ -87,6 +87,8 @@ export const ACHIEVEMENT_TYPES = [ 'brainDiver', 'smashTestNotificationButton', 'tutorialCompleted', + 'bubbleGameExplodingHead', + 'bubbleGameDoubleExplodingHead', ] as const; @Injectable() diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index f64196f4fc..6c5ee4835d 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -73,6 +73,37 @@ export class CaptchaService { } } + // https://codeberg.org/Gusted/mCaptcha/src/branch/main/mcaptcha.go + @bindThis + public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise<void> { + if (response == null) { + throw new Error('mcaptcha-failed: no response provided'); + } + + const endpointUrl = new URL('/api/v1/pow/siteverify', instanceHost); + const result = await this.httpRequestService.send(endpointUrl.toString(), { + method: 'POST', + body: JSON.stringify({ + key: siteKey, + secret: secret, + token: response, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (result.status !== 200) { + throw new Error('mcaptcha-failed: mcaptcha didn\'t return 200 OK'); + } + + const resp = (await result.json()) as { valid: boolean }; + + if (!resp.valid) { + throw new Error('mcaptcha-request-failed'); + } + } + @bindThis public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> { if (response == null) { diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 9b6187be4f..fc1927bfa6 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto'; import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import sharp from 'sharp'; -import { sharpBmp } from 'sharp-read-bmp'; +import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; import { IsNull } from 'typeorm'; import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; @@ -634,7 +634,7 @@ export class DriveService { public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) { const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw; - if (values.name && !this.driveFileEntityService.validateFileName(file.name)) { + if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) { throw new DriveService.InvalidFileNameError(); } diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 7fc7800783..7e812b4df2 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -156,7 +156,7 @@ export class EmailService { @bindThis public async validateEmailForAccount(emailAddress: string): Promise<{ available: boolean; - reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned'; + reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist'; }> { const meta = await this.metaService.fetch(); @@ -173,6 +173,8 @@ export class EmailService { if (meta.enableActiveEmailValidation) { if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) { validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey); + } else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) { + validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey); } else { validated = await validateEmail({ email: emailAddress, @@ -201,6 +203,8 @@ export class EmailService { validated.reason === 'disposable' ? 'disposable' : validated.reason === 'mx' ? 'mx' : validated.reason === 'smtp' ? 'smtp' : + validated.reason === 'network' ? 'network' : + validated.reason === 'blacklist' ? 'blacklist' : null, }; } @@ -265,4 +269,67 @@ export class EmailService { reason: null, }; } + + private async trueMail<T>(truemailInstance: string, emailAddress: string, truemailAuthKey: string): Promise<{ + valid: boolean; + reason: 'used' | 'format' | 'blacklist' | 'mx' | 'smtp' | 'network' | T | null; + }> { + const endpoint = truemailInstance + '?email=' + emailAddress; + try { + const res = await this.httpRequestService.send(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: truemailAuthKey + }, + }); + + const json = (await res.json()) as { + email: string; + success: boolean; + errors?: { + list_match?: string; + regex?: string; + mx?: string; + smtp?: string; + } | null; + }; + + if (json.email === undefined || (json.email !== undefined && json.errors?.regex)) { + return { + valid: false, + reason: 'format', + }; + } + if (json.errors?.smtp) { + return { + valid: false, + reason: 'smtp', + }; + } + if (json.errors?.mx) { + return { + valid: false, + reason: 'mx', + }; + } + if (!json.success) { + return { + valid: false, + reason: json.errors?.list_match as T || 'blacklist', + }; + } + + return { + valid: true, + reason: null, + }; + } catch (error) { + return { + valid: false, + reason: 'network', + }; + } + } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 3bc4a29b99..58f3d3559b 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -58,6 +58,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { isReply } from '@/misc/is-reply.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -862,7 +863,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.relayService.deliverToRelays(user, noteActivity); } - dm.execute(); + trackPromise(dm.execute()); })(); } //#endregion diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index 03c1735e04..c73cf76592 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; @Injectable() export class NoteReadService implements OnApplicationShutdown { @@ -107,7 +108,7 @@ export class NoteReadService implements OnApplicationShutdown { // TODO: ↓まとめてクエリしたい - this.noteUnreadsRepository.countBy({ + trackPromise(this.noteUnreadsRepository.countBy({ userId: userId, isMentioned: true, }).then(mentionsCount => { @@ -115,9 +116,9 @@ export class NoteReadService implements OnApplicationShutdown { // 全て既読になったイベントを発行 this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); } - }); + })); - this.noteUnreadsRepository.countBy({ + trackPromise(this.noteUnreadsRepository.countBy({ userId: userId, isSpecified: true, }).then(specifiedCount => { @@ -125,7 +126,7 @@ export class NoteReadService implements OnApplicationShutdown { // 全て既読になったイベントを発行 this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); } - }); + })); } } diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index ad7be83e5b..765fcae063 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -20,6 +20,7 @@ import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import { UserListService } from '@/core/UserListService.js'; import type { FilterUnionByProperty } from '@/types.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; @Injectable() export class NotificationService implements OnApplicationShutdown { @@ -74,7 +75,18 @@ export class NotificationService implements OnApplicationShutdown { } @bindThis - public async createNotification<T extends MiNotification['type']>( + public createNotification<T extends MiNotification['type']>( + notifieeId: MiUser['id'], + type: T, + data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>, + notifierId?: MiUser['id'] | null, + ) { + trackPromise( + this.#createNotificationInternal(notifieeId, type, data, notifierId), + ); + } + + async #createNotificationInternal<T extends MiNotification['type']>( notifieeId: MiUser['id'], type: T, data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>, diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index 4444dc9787..20a53ff282 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -3,12 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { setTimeout } from 'node:timers/promises'; import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { QUEUE, baseQueueOptions } from '@/queue/const.js'; +import { allSettled } from '@/misc/promise-tracker.js'; import type { Provider } from '@nestjs/common'; import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js'; @@ -106,14 +106,9 @@ export class QueueModule implements OnApplicationShutdown { ) {} public async dispose(): Promise<void> { - if (process.env.NODE_ENV === 'test') { - // XXX: - // Shutting down the existing connections causes errors on Jest as - // Misskey has asynchronous postgres/redis connections that are not - // awaited. - // Let's wait for some random time for them to finish. - await setTimeout(5000); - } + // Wait for all potential queue jobs + await allSettled(); + // And then close all queues await Promise.all([ this.systemQueue.close(), this.endedPollNotificationQueue.close(), diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 2ee61eb549..e1c84535a0 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -17,6 +17,7 @@ import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '. import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; import { MiNote } from '@/models/Note.js'; +import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; @Injectable() export class QueueService { @@ -75,11 +76,15 @@ export class QueueService { if (content == null) return null; if (to == null) return null; + const contentBody = JSON.stringify(content); + const digest = ApRequestCreator.createDigest(contentBody); + const data: DeliverJobData = { user: { id: user.id, }, - content, + content: contentBody, + digest, to, isSharedInbox, }; @@ -104,6 +109,8 @@ export class QueueService { @bindThis public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) { if (content == null) return null; + const contentBody = JSON.stringify(content); + const digest = ApRequestCreator.createDigest(contentBody); const opts = { attempts: this.config.deliverJobMaxAttempts ?? 12, @@ -118,7 +125,8 @@ export class QueueService { name: d[0], data: { user, - content, + content: contentBody, + digest, to: d[0], isSharedInbox: d[1], } as DeliverJobData, @@ -186,6 +194,16 @@ export class QueueService { } @bindThis + public createExportClipsJob(user: ThinUser) { + return this.dbQueue.add('exportClips', { + user: { id: user.id }, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + + @bindThis public createExportFavoritesJob(user: ThinUser) { return this.dbQueue.add('exportFavorites', { user: { id: user.id }, diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 0daee34ce5..11c972982e 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -28,6 +28,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { RoleService } from '@/core/RoleService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; const FALLBACK = '❤'; const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; @@ -280,7 +281,7 @@ export class ReactionService { } } - dm.execute(); + trackPromise(dm.execute()); } //#endregion } @@ -328,7 +329,7 @@ export class ReactionService { dm.addDirectRecipe(reactee as MiRemoteUser); } dm.addFollowersRecipe(); - dm.execute(); + trackPromise(dm.execute()); } //#endregion } diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 81003bcf1c..d7414e9c99 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -144,7 +144,7 @@ class DeliverManager { } // deliver - this.queueService.deliverMany(this.actor, this.activity, inboxes); + await this.queueService.deliverMany(this.actor, this.activity, inboxes); } } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index d8616d293d..3f01c0289a 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -97,6 +97,8 @@ export class ApInboxService { } catch (err) { if (err instanceof Error || typeof err === 'string') { this.logger.error(err); + } else { + throw err; } } } @@ -256,7 +258,7 @@ export class ApInboxService { const targetUri = getApId(activity.object); - this.announceNote(actor, activity, targetUri); + await this.announceNote(actor, activity, targetUri); } @bindThis @@ -288,7 +290,7 @@ export class ApInboxService { } catch (err) { // 対象が4xxならスキップ if (err instanceof StatusError) { - if (err.isClientError) { + if (!err.isRetryable) { this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`); return; } @@ -373,7 +375,7 @@ export class ApInboxService { }); if (isPost(object)) { - this.createNote(resolver, actor, object, false, activity); + await this.createNote(resolver, actor, object, false, activity); } else { this.logger.warn(`Unknown type: ${getApType(object)}`); } @@ -404,7 +406,7 @@ export class ApInboxService { await this.apNoteService.createNote(note, resolver, silent); return 'ok'; } catch (err) { - if (err instanceof StatusError && err.isClientError) { + if (err instanceof StatusError && !err.isRetryable) { return `skip ${err.statusCode}`; } else { throw err; diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index b59ce5241f..e165c5e960 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -34,9 +34,9 @@ type PrivateKey = { }; export class ApRequestCreator { - static createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }): Signed { + static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record<string, string> }): Signed { const u = new URL(args.url); - const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`; + const digestHeader = args.digest ?? this.createDigest(args.body); const request: Request = { url: u.href, @@ -59,6 +59,10 @@ export class ApRequestCreator { }; } + static createDigest(body: string) { + return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`; + } + static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed { const u = new URL(args.url); @@ -145,8 +149,8 @@ export class ApRequestService { } @bindThis - public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown): Promise<void> { - const body = JSON.stringify(object); + public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise<void> { + const body = typeof object === 'string' ? object : JSON.stringify(object); const keypair = await this.userKeypairService.getUserKeypair(user.id); @@ -157,6 +161,7 @@ export class ApRequestService { }, url, body, + digest, additionalHeaders: { }, }); diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 2595783e04..12958811ed 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -221,7 +221,7 @@ export class ApNoteService { return { status: 'ok', res }; } catch (e) { return { - status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror', + status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror', }; } }; diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index fee96bb80d..a5d3054462 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -369,6 +369,7 @@ export class NoteEntityService implements OnModuleInit { color: channel.color, isSensitive: channel.isSensitive, allowRenoteToExternal: channel.allowRenoteToExternal, + userId: channel.userId, } : undefined, mentions: note.mentions && note.mentions.length > 0 ? note.mentions : undefined, uri: note.uri ?? undefined, diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index c5ef9b2fa3..4c55acea5a 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -37,7 +37,7 @@ export class ServerStatsService implements OnApplicationShutdown { const log = [] as any[]; ev.on('requestServerStatsLog', x => { - ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length ?? 50)); + ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length)); }); const tick = async () => { diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index 5c10559ec6..0a19036c97 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -71,8 +71,11 @@ export default class Logger { let log = `${l} ${worker}\t[${contexts.join(' ')}]\t${m}`; if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log; - console.log(important ? chalk.bold(log) : log); - if (level === 'error' && data) console.log(data); + const args: unknown[] = [important ? chalk.bold(log) : log]; + if (data != null) { + args.push(data); + } + console.log(...args); } @bindThis diff --git a/packages/backend/src/misc/promise-tracker.ts b/packages/backend/src/misc/promise-tracker.ts new file mode 100644 index 0000000000..c7166c6de9 --- /dev/null +++ b/packages/backend/src/misc/promise-tracker.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const promiseRefs: Set<WeakRef<Promise<unknown>>> = new Set(); + +/** + * This tracks promises that other modules decided not to wait for, + * and makes sure they are all settled before fully closing down the server. + */ +export function trackPromise(promise: Promise<unknown>) { + if (process.env.NODE_ENV !== 'test') { + return; + } + const ref = new WeakRef(promise); + promiseRefs.add(ref); + promise.finally(() => promiseRefs.delete(ref)); +} + +export async function allSettled(): Promise<void> { + await Promise.allSettled([...promiseRefs].map(r => r.deref())); +} diff --git a/packages/backend/src/misc/status-error.ts b/packages/backend/src/misc/status-error.ts index 4285685d24..be213088a8 100644 --- a/packages/backend/src/misc/status-error.ts +++ b/packages/backend/src/misc/status-error.ts @@ -7,6 +7,7 @@ export class StatusError extends Error { public statusCode: number; public statusMessage?: string; public isClientError: boolean; + public isRetryable: boolean; constructor(message: string, statusCode: number, statusMessage?: string) { super(message); @@ -14,5 +15,6 @@ export class StatusError extends Error { this.statusCode = statusCode; this.statusMessage = statusMessage; this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500; + this.isRetryable = !this.isClientError || this.statusCode === 429; } } diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 4bf856e619..e7f7458c19 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -199,6 +199,29 @@ export class MiMeta { @Column('boolean', { default: false, }) + public enableMcaptcha: boolean; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public mcaptchaSitekey: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public mcaptchaSecretKey: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public mcaptchaInstanceUrl: string | null; + + @Column('boolean', { + default: false, + }) public enableRecaptcha: boolean; @Column('varchar', { @@ -468,6 +491,23 @@ export class MiMeta { public verifymailAuthKey: string | null; @Column('boolean', { + default: false, + }) + public enableTruemailApi: boolean; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public truemailInstance: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public truemailAuthKey: string | null; + + @Column('boolean', { default: true, }) public enableChartsForRemoteUser: boolean; diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index aa749943f0..2b7722129b 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -148,6 +148,10 @@ export const packedNoteSchema = { type: 'boolean', optional: false, nullable: false, }, + userId: { + type: 'string', + optional: false, nullable: true, + }, }, }, localOnly: { diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 29dc78605b..d547a498a1 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -25,6 +25,7 @@ import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmo import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; +import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js'; import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js'; import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js'; import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js'; @@ -56,6 +57,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor ExportAccountDataProcessorService, ExportCustomEmojisProcessorService, ExportNotesProcessorService, + ExportClipsProcessorService, ExportFavoritesProcessorService, ExportFollowingProcessorService, ExportMutingProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index ea3ecd4ded..cdca744b88 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -17,6 +17,7 @@ import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesP import { ExportAccountDataProcessorService } from './processors/ExportAccountDataProcessorService.js'; import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js'; import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; +import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js'; import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js'; @@ -94,6 +95,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private exportAccountDataProcessorService: ExportAccountDataProcessorService, private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService, private exportNotesProcessorService: ExportNotesProcessorService, + private exportClipsProcessorService: ExportClipsProcessorService, private exportFavoritesProcessorService: ExportFavoritesProcessorService, private exportFollowingProcessorService: ExportFollowingProcessorService, private exportMutingProcessorService: ExportMutingProcessorService, @@ -169,6 +171,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'exportAccountData': return this.exportAccountDataProcessorService.process(job); case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job); case 'exportNotes': return this.exportNotesProcessorService.process(job); + case 'exportClips': return this.exportClipsProcessorService.process(job); case 'exportFavorites': return this.exportFavoritesProcessorService.process(job); case 'exportFollowing': return this.exportFollowingProcessorService.process(job); case 'exportMuting': return this.exportMutingProcessorService.process(job); diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 4a1d9f28b4..64c3445552 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -72,7 +72,7 @@ export class DeliverProcessorService { } try { - await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content); + await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest); // Update stats this.federatedInstanceService.fetch(host).then(i => { @@ -111,7 +111,7 @@ export class DeliverProcessorService { if (res instanceof StatusError) { // 4xx - if (res.isClientError) { + if (!res.isRetryable) { // 相手が閉鎖していることを明示しているため、配送停止する if (job.data.isSharedInbox && res.statusCode === 410) { this.federatedInstanceService.fetch(host).then(i => { diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts new file mode 100644 index 0000000000..5221497bd3 --- /dev/null +++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts @@ -0,0 +1,206 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as fs from 'node:fs'; +import { Writable } from 'node:stream'; +import { Inject, Injectable, StreamableFile } from '@nestjs/common'; +import { MoreThan } from 'typeorm'; +import { format as dateFormat } from 'date-fns'; +import { DI } from '@/di-symbols.js'; +import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { createTemp } from '@/misc/create-temp.js'; +import type { MiPoll } from '@/models/Poll.js'; +import type { MiNote } from '@/models/Note.js'; +import { bindThis } from '@/decorators.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { Packed } from '@/misc/json-schema.js'; +import { IdService } from '@/core/IdService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbJobDataWithUser } from '../types.js'; + +@Injectable() +export class ExportClipsProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, + + private driveService: DriveService, + private queueLoggerService: QueueLoggerService, + private idService: IdService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('export-clips'); + } + + @bindThis + public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> { + this.logger.info(`Exporting clips of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + return; + } + + // Create temp file + const [path, cleanup] = await createTemp(); + + this.logger.info(`Temp file is ${path}`); + + try { + const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' })); + const writer = stream.getWriter(); + writer.closed.catch(this.logger.error); + + await writer.write('['); + + await this.processClips(writer, user, job); + + await writer.write(']'); + await writer.close(); + + this.logger.succ(`Exported to: ${path}`); + + const fileName = 'clips-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; + const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); + + this.logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } + } + + async processClips(writer: WritableStreamDefaultWriter, user: MiUser, job: Bull.Job<DbJobDataWithUser>) { + let exportedClipsCount = 0; + let cursor: MiClip['id'] | null = null; + + while (true) { + const clips = await this.clipsRepository.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }); + + if (clips.length === 0) { + job.updateProgress(100); + break; + } + + cursor = clips.at(-1)?.id ?? null; + + for (const clip of clips) { + // Stringify but remove the last `]}` + const content = JSON.stringify(this.serializeClip(clip)).slice(0, -2); + const isFirst = exportedClipsCount === 0; + await writer.write(isFirst ? content : ',\n' + content); + + await this.processClipNotes(writer, clip.id); + + await writer.write(']}'); + exportedClipsCount++; + } + + const total = await this.clipsRepository.countBy({ + userId: user.id, + }); + + job.updateProgress(exportedClipsCount / total); + } + } + + async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise<void> { + let exportedClipNotesCount = 0; + let cursor: MiClipNote['id'] | null = null; + + while (true) { + const clipNotes = await this.clipNotesRepository.find({ + where: { + clipId, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + relations: ['note', 'note.user'], + }) as (MiClipNote & { note: MiNote & { user: MiUser } })[]; + + if (clipNotes.length === 0) { + break; + } + + cursor = clipNotes.at(-1)?.id ?? null; + + for (const clipNote of clipNotes) { + let poll: MiPoll | undefined; + if (clipNote.note.hasPoll) { + poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id }); + } + const content = JSON.stringify(this.serializeClipNote(clipNote, poll)); + const isFirst = exportedClipNotesCount === 0; + await writer.write(isFirst ? content : ',\n' + content); + + exportedClipNotesCount++; + } + } + } + + private serializeClip(clip: MiClip): Record<string, unknown> { + return { + id: clip.id, + name: clip.name, + description: clip.description, + lastClippedAt: clip.lastClippedAt?.toISOString(), + clipNotes: [], + }; + } + + private serializeClipNote(clip: MiClipNote & { note: MiNote & { user: MiUser } }, poll: MiPoll | undefined): Record<string, unknown> { + return { + id: clip.id, + createdAt: this.idService.parse(clip.id).date.toISOString(), + note: { + id: clip.note.id, + text: clip.note.text, + createdAt: this.idService.parse(clip.note.id).date.toISOString(), + fileIds: clip.note.fileIds, + replyId: clip.note.replyId, + renoteId: clip.note.renoteId, + poll: poll, + cw: clip.note.cw, + visibility: clip.note.visibility, + visibleUserIds: clip.note.visibleUserIds, + localOnly: clip.note.localOnly, + reactionAcceptance: clip.note.reactionAcceptance, + uri: clip.note.uri, + url: clip.note.url, + user: { + id: clip.note.user.id, + name: clip.note.user.name, + username: clip.note.user.username, + host: clip.note.user.host, + uri: clip.note.user.uri, + }, + }, + }; + } +} diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index f69634968d..971e9f4971 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -85,7 +85,7 @@ export class InboxProcessorService { } catch (err) { // 対象が4xxならスキップ if (err instanceof StatusError) { - if (err.isClientError) { + if (!err.isRetryable) { throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`); } throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`); diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts index a41f5565c8..7a0d533846 100644 --- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts @@ -71,7 +71,7 @@ export class WebhookDeliverProcessorService { if (res instanceof StatusError) { // 4xx - if (res.isClientError) { + if (!res.isRetryable) { throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); } diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 432b3d364f..372829a825 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -15,7 +15,9 @@ export type DeliverJobData = { /** Actor */ user: ThinUser; /** Activity */ - content: unknown; + content: string; + /** Digest header */ + digest: string; /** inbox URL to deliver */ to: string; /** whether it is sharedInbox */ diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index e82ef64dc4..61e8e8c841 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -9,7 +9,7 @@ import { dirname } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import rename from 'rename'; import sharp from 'sharp'; -import { sharpBmp } from 'sharp-read-bmp'; +import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; import type { Config } from '@/config.js'; import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index ed1b2d4377..f77c50012d 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -214,6 +214,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportMute from './endpoints/i/export-mute.js'; import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; +import * as ep___i_exportClips from './endpoints/i/export-clips.js'; import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; @@ -588,6 +589,7 @@ const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default }; const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default }; const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default }; +const $i_exportClips: Provider = { provide: 'ep:i/export-clips', useClass: ep___i_exportClips.default }; const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default }; const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default }; const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default }; @@ -966,6 +968,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $i_exportFollowing, $i_exportMute, $i_exportNotes, + $i_exportClips, $i_exportFavorites, $i_exportUserLists, $i_exportAntennas, @@ -1338,6 +1341,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $i_exportFollowing, $i_exportMute, $i_exportNotes, + $i_exportClips, $i_exportFavorites, $i_exportUserLists, $i_exportAntennas, diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 63379c8878..8788a1fd64 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -70,6 +70,7 @@ export class SignupApiService { 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; + 'm-captcha-response'?: string; } }>, reply: FastifyReply, @@ -87,6 +88,12 @@ export class SignupApiService { }); } + if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) { + await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + if (instance.enableRecaptcha && instance.recaptchaSecretKey) { await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index f82bf257fc..1f59eeb012 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { permissions } from 'misskey-js'; import type { Schema } from '@/misc/json-schema.js'; import { permissions } from 'misskey-js'; import { RolePolicies } from '@/core/RoleService.js'; @@ -215,6 +216,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportMute from './endpoints/i/export-mute.js'; import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; +import * as ep___i_exportClips from './endpoints/i/export-clips.js'; import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; @@ -587,6 +589,7 @@ const eps = [ ['i/export-following', ep___i_exportFollowing], ['i/export-mute', ep___i_exportMute], ['i/export-notes', ep___i_exportNotes], + ['i/export-clips', ep___i_exportClips], ['i/export-favorites', ep___i_exportFavorites], ['i/export-user-lists', ep___i_exportUserLists], ['i/export-antennas', ep___i_exportAntennas], diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 4fd2a568ad..66b6799ed1 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -45,6 +45,18 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + enableMcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, + mcaptchaSiteKey: { + type: 'string', + optional: false, nullable: true, + }, + mcaptchaInstanceUrl: { + type: 'string', + optional: false, nullable: true, + }, enableRecaptcha: { type: 'boolean', optional: false, nullable: false, @@ -174,6 +186,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + mcaptchaSecretKey: { + type: 'string', + optional: false, nullable: true, + }, recaptchaSecretKey: { type: 'string', optional: false, nullable: true, @@ -299,6 +315,18 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + enableTruemailApi: { + type: 'boolean', + optional: false, nullable: false, + }, + truemailInstance: { + type: 'string', + optional: false, nullable: true, + }, + truemailAuthKey: { + type: 'string', + optional: false, nullable: true, + }, enableChartsForRemoteUser: { type: 'boolean', optional: false, nullable: false, @@ -476,6 +504,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- approvalRequiredForSignup: instance.approvalRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, + enableMcaptcha: instance.enableMcaptcha, + mcaptchaSiteKey: instance.mcaptchaSitekey, + mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl, enableRecaptcha: instance.enableRecaptcha, recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, @@ -508,6 +539,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- preservedUsernames: instance.preservedUsernames, bubbleInstances: instance.bubbleInstances, hcaptchaSecretKey: instance.hcaptchaSecretKey, + mcaptchaSecretKey: instance.mcaptchaSecretKey, recaptchaSecretKey: instance.recaptchaSecretKey, turnstileSecretKey: instance.turnstileSecretKey, sensitiveMediaDetection: instance.sensitiveMediaDetection, @@ -543,6 +575,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- enableActiveEmailValidation: instance.enableActiveEmailValidation, enableVerifymailApi: instance.enableVerifymailApi, verifymailAuthKey: instance.verifymailAuthKey, + enableTruemailApi: instance.enableTruemailApi, + truemailInstance: instance.truemailInstance, + truemailAuthKey: instance.truemailAuthKey, enableChartsForRemoteUser: instance.enableChartsForRemoteUser, enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances, enableServerMachineStats: instance.enableServerMachineStats, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 5c916fe340..05d2cd61ca 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -65,6 +65,10 @@ export const paramDef = { enableHcaptcha: { type: 'boolean' }, hcaptchaSiteKey: { type: 'string', nullable: true }, hcaptchaSecretKey: { type: 'string', nullable: true }, + enableMcaptcha: { type: 'boolean' }, + mcaptchaSiteKey: { type: 'string', nullable: true }, + mcaptchaInstanceUrl: { type: 'string', nullable: true }, + mcaptchaSecretKey: { type: 'string', nullable: true }, enableRecaptcha: { type: 'boolean' }, recaptchaSiteKey: { type: 'string', nullable: true }, recaptchaSecretKey: { type: 'string', nullable: true }, @@ -119,6 +123,9 @@ export const paramDef = { enableActiveEmailValidation: { type: 'boolean' }, enableVerifymailApi: { type: 'boolean' }, verifymailAuthKey: { type: 'string', nullable: true }, + enableTruemailApi: { type: 'boolean' }, + truemailInstance: { type: 'string', nullable: true }, + truemailAuthKey: { type: 'string', nullable: true }, enableChartsForRemoteUser: { type: 'boolean' }, enableChartsForFederatedInstances: { type: 'boolean' }, enableServerMachineStats: { type: 'boolean' }, @@ -279,6 +286,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.hcaptchaSecretKey = ps.hcaptchaSecretKey; } + if (ps.enableMcaptcha !== undefined) { + set.enableMcaptcha = ps.enableMcaptcha; + } + + if (ps.mcaptchaSiteKey !== undefined) { + set.mcaptchaSitekey = ps.mcaptchaSiteKey; + } + + if (ps.mcaptchaInstanceUrl !== undefined) { + set.mcaptchaInstanceUrl = ps.mcaptchaInstanceUrl; + } + + if (ps.mcaptchaSecretKey !== undefined) { + set.mcaptchaSecretKey = ps.mcaptchaSecretKey; + } + if (ps.enableRecaptcha !== undefined) { set.enableRecaptcha = ps.enableRecaptcha; } @@ -471,6 +494,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } + if (ps.enableTruemailApi !== undefined) { + set.enableTruemailApi = ps.enableTruemailApi; + } + + if (ps.truemailInstance !== undefined) { + if (ps.truemailInstance === '') { + set.truemailInstance = null; + } else { + set.truemailInstance = ps.truemailInstance; + } + } + + if (ps.truemailAuthKey !== undefined) { + if (ps.truemailAuthKey === '') { + set.truemailAuthKey = null; + } else { + set.truemailAuthKey = ps.truemailAuthKey; + } + } + if (ps.enableChartsForRemoteUser !== undefined) { set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser; } diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 0bf2688b4a..7293c2e39b 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -14,6 +14,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- antenna.isActive = true; antenna.lastUsedAt = new Date(); - this.antennasRepository.update(antenna.id, antenna); + trackPromise(this.antennasRepository.update(antenna.id, antenna)); if (needPublishEvent) { this.globalEventService.publishInternalEvent('antennaUpdated', antenna); diff --git a/packages/backend/src/server/api/endpoints/i/export-clips.ts b/packages/backend/src/server/api/endpoints/i/export-clips.ts new file mode 100644 index 0000000000..9435a2b23c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/export-clips.ts @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; + +export const meta = { + secure: true, + requireCredential: true, + limit: { + duration: ms('1day'), + max: 1, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportClipsJob(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 1d0c102c9d..9d2ae8369c 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -115,6 +115,18 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + enableMcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, + mcaptchaSiteKey: { + type: 'string', + optional: false, nullable: true, + }, + mcaptchaInstanceUrl: { + type: 'string', + optional: false, nullable: true, + }, enableRecaptcha: { type: 'boolean', optional: false, nullable: false, @@ -359,6 +371,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- approvalRequiredForSignup: instance.approvalRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, + enableMcaptcha: instance.enableMcaptcha, + mcaptchaSiteKey: instance.mcaptchaSitekey, + mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl, enableRecaptcha: instance.enableRecaptcha, enableAchievements: instance.enableAchievements, recaptchaSiteKey: instance.recaptchaSiteKey, diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 909b5a5e03..e0245814c4 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -21,6 +21,7 @@ class UserListChannel extends Channel { private membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {}; private listUsersClock: NodeJS.Timeout; private withFiles: boolean; + private withRenotes: boolean; constructor( private userListsRepository: UserListsRepository, @@ -39,6 +40,7 @@ class UserListChannel extends Channel { public async init(params: any) { this.listId = params.listId as string; this.withFiles = params.withFiles ?? false; + this.withRenotes = params.withRenotes ?? true; // Check existence and owner const listExist = await this.userListsRepository.exist({ @@ -104,6 +106,8 @@ class UserListChannel extends Channel { } } + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index d590244e34..3fd88355dd 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { summaly } from 'summaly'; +import { summaly } from '@misskey-dev/summaly'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; diff --git a/packages/backend/test-server/.eslintrc.cjs b/packages/backend/test-server/.eslintrc.cjs new file mode 100644 index 0000000000..c261741a36 --- /dev/null +++ b/packages/backend/test-server/.eslintrc.cjs @@ -0,0 +1,32 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + extends: [ + '../../shared/.eslintrc.js', + ], + rules: { + 'import/order': ['warn', { + 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], + 'pathGroups': [ + { + 'pattern': '@/**', + 'group': 'external', + 'position': 'after' + } + ], + }], + 'no-restricted-globals': [ + 'error', + { + 'name': '__dirname', + 'message': 'Not in ESModule. Use `import.meta.url` instead.' + }, + { + 'name': '__filename', + 'message': 'Not in ESModule. Use `import.meta.url` instead.' + } + ] + }, +}; diff --git a/packages/backend/test-server/.swcrc b/packages/backend/test-server/.swcrc new file mode 100644 index 0000000000..e3d6935169 --- /dev/null +++ b/packages/backend/test-server/.swcrc @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript", + "dynamicImport": true, + "decorators": true + }, + "transform": { + "legacyDecorator": true, + "decoratorMetadata": true + }, + "experimental": { + "keepImportAssertions": true + }, + "baseUrl": "../built", + "paths": { + "@/*": ["*"] + }, + "target": "es2022" + }, + "minify": false +} diff --git a/packages/backend/test-server/entry.ts b/packages/backend/test-server/entry.ts new file mode 100644 index 0000000000..866a7e1f5b --- /dev/null +++ b/packages/backend/test-server/entry.ts @@ -0,0 +1,80 @@ +import { portToPid } from 'pid-port'; +import fkill from 'fkill'; +import Fastify from 'fastify'; +import { NestFactory } from '@nestjs/core'; +import { MainModule } from '@/MainModule.js'; +import { ServerService } from '@/server/ServerService.js'; +import { loadConfig } from '@/config.js'; +import { NestLogger } from '@/NestLogger.js'; + +const config = loadConfig(); +const originEnv = JSON.stringify(process.env); + +process.env.NODE_ENV = 'test'; + +/** + * テスト用のサーバインスタンスを起動する + */ +async function launch() { + await killTestServer(); + + console.log('starting application...'); + + const app = await NestFactory.createApplicationContext(MainModule, { + logger: new NestLogger(), + }); + const serverService = app.get(ServerService); + await serverService.launch(); + + await startControllerEndpoints(); + + // ジョブキューは必要な時にテストコード側で起動する + // ジョブキューが動くとテスト結果の確認に支障が出ることがあるので意図的に動かさないでいる + + console.log('application initialized.'); +} + +/** + * 既に重複したポートで待ち受けしているサーバがある場合はkillする + */ +async function killTestServer() { + // + try { + const pid = await portToPid(config.port); + if (pid) { + await fkill(pid, { force: true }); + } + } catch { + // NOP; + } +} + +/** + * 別プロセスに切り離してしまったが故に出来なくなった環境変数の書き換え等を実現するためのエンドポイントを作る + * @param port + */ +async function startControllerEndpoints(port = config.port + 1000) { + const fastify = Fastify(); + + fastify.post<{ Body: { key?: string, value?: string } }>('/env', async (req, res) => { + console.log(req.body); + const key = req.body['key']; + if (!key) { + res.code(400).send({ success: false }); + return; + } + + process.env[key] = req.body['value']; + + res.code(200).send({ success: true }); + }); + + fastify.post<{ Body: { key?: string, value?: string } }>('/env-reset', async (req, res) => { + process.env = JSON.parse(originEnv); + res.code(200).send({ success: true }); + }); + + await fastify.listen({ port: port, host: 'localhost' }); +} + +export default launch; diff --git a/packages/backend/test-server/tsconfig.json b/packages/backend/test-server/tsconfig.json new file mode 100644 index 0000000000..10313699c2 --- /dev/null +++ b/packages/backend/test-server/tsconfig.json @@ -0,0 +1,52 @@ +{ + "compilerOptions": { + "allowJs": true, + "noEmitOnError": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedParameters": false, + "noUnusedLocals": false, + "noFallthroughCasesInSwitch": true, + "declaration": false, + "sourceMap": true, + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "allowSyntheticDefaultImports": true, + "removeComments": false, + "noLib": false, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "resolveJsonModule": true, + "isolatedModules": true, + "rootDir": "../src", + "baseUrl": "./", + "paths": { + "@/*": ["../src/*"] + }, + "outDir": "../built-test", + "types": [ + "node" + ], + "typeRoots": [ + "../src/@types", + "../node_modules/@types", + "../node_modules" + ], + "lib": [ + "esnext" + ] + }, + "compileOnSave": false, + "include": [ + "./**/*.ts", + "../src/**/*.ts" + ], + "exclude": [ + "../src/**/*.test.ts" + ] +} diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index ed967d2620..165a1055c9 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -10,7 +10,7 @@ import * as crypto from 'node:crypto'; import cbor from 'cbor'; import * as OTPAuth from 'otpauth'; import { loadConfig } from '@/config.js'; -import { api, signup, startServer } from '../utils.js'; +import { api, signup } from '../utils.js'; import type { AuthenticationResponseJSON, AuthenticatorAssertionResponseJSON, @@ -19,12 +19,10 @@ import type { PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, } from '@simplewebauthn/typescript-types'; -import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('2要素認証', () => { - let app: INestApplicationContext; - let alice: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; const config = loadConfig(); const password = 'test'; @@ -185,14 +183,9 @@ describe('2要素認証', () => { }; beforeAll(async () => { - app = await startServer(); alice = await signup({ username, password }); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - test('が設定でき、OTPでログインできる。', async () => { const registerResponse = await api('/i/2fa/register', { password, diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index c0317f1435..e63722b246 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -6,24 +6,20 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { inspect } from 'node:util'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import type { Packed } from '@/misc/json-schema.js'; import { - signup, + api, + failedApiCall, post, - userList, - page, role, - startServer, - api, + signup, successfulApiCall, - failedApiCall, - uploadFile, testPaginationConsistency, + uploadFile, + userList, } from '../utils.js'; import type * as misskey from 'misskey-js'; -import type { INestApplicationContext } from '@nestjs/common'; const compareBy = <T extends { id: string }>(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => { return selector(a).localeCompare(selector(b)); @@ -37,7 +33,7 @@ describe('アンテナ', () => { // - srcのenumにgroupが残っている // - userGroupIdが残っている, isActiveがない type Antenna = misskey.entities.Antenna | Packed<'Antenna'>; - type User = misskey.entities.MeSignup; + type User = misskey.entities.SignupResponse; type Note = misskey.entities.Note; // アンテナを作成できる最小のパラメタ @@ -54,8 +50,6 @@ describe('アンテナ', () => { withReplies: false, }; - let app: INestApplicationContext; - let root: User; let alice: User; let bob: User; @@ -80,10 +74,6 @@ describe('アンテナ', () => { let userMutedByAlice: User; beforeAll(async () => { - app = await startServer(); - }, 1000 * 60 * 2); - - beforeAll(async () => { root = await signup({ username: 'root' }); alice = await signup({ username: 'alice' }); alicePost = await post(alice, { text: 'test' }); @@ -136,10 +126,6 @@ describe('アンテナ', () => { await api('mute/create', { userId: userMutedByAlice.id }, alice); }, 1000 * 60 * 10); - afterAll(async () => { - await app.close(); - }); - beforeEach(async () => { // テスト間で影響し合わないように毎回全部消す。 for (const user of [alice, bob]) { diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts index 33c8d03fdb..89d8b42271 100644 --- a/packages/backend/test/e2e/api-visibility.ts +++ b/packages/backend/test/e2e/api-visibility.ts @@ -6,33 +6,22 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, startServer } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, post, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('API visibility', () => { - let app: INestApplicationContext; - - beforeAll(async () => { - app = await startServer(); - }, 1000 * 60 * 2); - - afterAll(async () => { - await app.close(); - }); - describe('Note visibility', () => { //#region vars /** ヒロイン */ - let alice: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; /** フォロワー */ - let follower: misskey.entities.MeSignup; + let follower: misskey.entities.SignupResponse; /** 非フォロワー */ - let other: misskey.entities.MeSignup; + let other: misskey.entities.SignupResponse; /** 非フォロワーでもリプライやメンションをされた人 */ - let target: misskey.entities.MeSignup; + let target: misskey.entities.SignupResponse; /** specified mentionでmentionを飛ばされる人 */ - let target2: misskey.entities.MeSignup; + let target2: misskey.entities.SignupResponse; /** public-post */ let pub: any; diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index cf24228b83..25d5bdb175 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -7,27 +7,30 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { IncomingMessage } from 'http'; -import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch, createAppToken } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { + api, + connectStream, + createAppToken, + failedApiCall, + relativeFetch, + signup, + successfulApiCall, + uploadFile, + waitFire, +} from '../utils.js'; import type * as misskey from 'misskey-js'; describe('API', () => { - let app: INestApplicationContext; - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - describe('General validation', () => { test('wrong type', async () => { const res = await api('/test', { diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts index 4445d9036c..1dfc87c64f 100644 --- a/packages/backend/test/e2e/block.ts +++ b/packages/backend/test/e2e/block.ts @@ -6,29 +6,21 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, startServer } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, post, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Block', () => { - let app: INestApplicationContext; - // alice blocks bob - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - test('Block作成', async () => { const res = await api('/blocking/create', { userId: bob.id, diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index 49092fba63..b679eea8cf 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -18,25 +18,13 @@ import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unf import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js'; import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js'; import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js'; -import { - signup, - post, - startServer, - api, - successfulApiCall, - failedApiCall, - ApiRequest, - hiddenNote, -} from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js'; describe('クリップ', () => { type User = Packed<'User'>; type Note = Packed<'Note'>; type Clip = Packed<'Clip'>; - let app: INestApplicationContext; - let alice: User; let bob: User; let aliceNote: Note; @@ -145,7 +133,6 @@ describe('クリップ', () => { }; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); @@ -160,10 +147,6 @@ describe('クリップ', () => { bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any; }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - afterEach(async () => { // テスト間で影響し合わないように毎回全部消す。 for (const user of [alice, bob]) { diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index 2ef3434bca..b12b062a63 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -10,30 +10,22 @@ import * as assert from 'assert'; // https://github.com/node-fetch/node-fetch/pull/1664 import { Blob } from 'node-fetch'; import { MiUser } from '@/models/_.js'; -import { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Endpoints', () => { - let app: INestApplicationContext; - - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; - let dave: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; + let dave: misskey.entities.SignupResponse; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); dave = await signup({ username: 'dave' }); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - describe('signup', () => { test('不正なユーザー名でアカウントが作成できない', async () => { const res = await api('signup', { @@ -710,6 +702,18 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 400); }); + test('不正なファイル名で怒られる', async () => { + const file = (await uploadFile(alice)).body; + const newName = ''; + + const res = await api('/drive/files/update', { + fileId: file.id, + name: newName, + }, alice); + + assert.strictEqual(res.status, 400); + }); + test('間違ったIDで怒られる', async () => { const res = await api('/drive/files/update', { fileId: 'kyoppie', diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts new file mode 100644 index 0000000000..f9b59144a3 --- /dev/null +++ b/packages/backend/test/e2e/exports.ts @@ -0,0 +1,193 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { api, port, post, signup, startJobQueue } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; + +describe('export-clips', () => { + let queue: INestApplicationContext; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + + // XXX: Any better way to get the result? + async function pollFirstDriveFile() { + while (true) { + const files = (await api('/drive/files', {}, alice)).body; + if (!files.length) { + await new Promise(r => setTimeout(r, 100)); + continue; + } + if (files.length > 1) { + throw new Error('Too many files?'); + } + const file = (await api('/drive/files/show', { fileId: files[0].id }, alice)).body; + const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`)); + return await res.json(); + } + } + + beforeAll(async () => { + queue = await startJobQueue(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await queue.close(); + }); + + beforeEach(async () => { + // Clean all clips and files of alice + const clips = (await api('/clips/list', {}, alice)).body; + for (const clip of clips) { + const res = await api('/clips/delete', { clipId: clip.id }, alice); + if (res.status !== 204) { + throw new Error('Failed to delete clip'); + } + } + const files = (await api('/drive/files', {}, alice)).body; + for (const file of files) { + const res = await api('/drive/files/delete', { fileId: file.id }, alice); + if (res.status !== 204) { + throw new Error('Failed to delete file'); + } + } + }); + + test('basic export', async () => { + let res = await api('/clips/create', { + name: 'foo', + description: 'bar', + }, alice); + assert.strictEqual(res.status, 200); + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].name, 'foo'); + assert.strictEqual(exported[0].description, 'bar'); + assert.strictEqual(exported[0].clipNotes.length, 0); + }); + + test('export with notes', async () => { + let res = await api('/clips/create', { + name: 'foo', + description: 'bar', + }, alice); + assert.strictEqual(res.status, 200); + const clip = res.body; + + const note1 = await post(alice, { + text: 'baz1', + }); + + const note2 = await post(alice, { + text: 'baz2', + poll: { + choices: ['sakura', 'izumi', 'ako'], + }, + }); + + for (const note of [note1, note2]) { + res = await api('/clips/add-note', { + clipId: clip.id, + noteId: note.id, + }, alice); + assert.strictEqual(res.status, 204); + } + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].name, 'foo'); + assert.strictEqual(exported[0].description, 'bar'); + assert.strictEqual(exported[0].clipNotes.length, 2); + assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1'); + assert.strictEqual(exported[0].clipNotes[1].note.text, 'baz2'); + assert.deepStrictEqual(exported[0].clipNotes[1].note.poll.choices[0], 'sakura'); + }); + + test('multiple clips', async () => { + let res = await api('/clips/create', { + name: 'kawaii', + description: 'kawaii', + }, alice); + assert.strictEqual(res.status, 200); + const clip1 = res.body; + + res = await api('/clips/create', { + name: 'yuri', + description: 'yuri', + }, alice); + assert.strictEqual(res.status, 200); + const clip2 = res.body; + + const note1 = await post(alice, { + text: 'baz1', + }); + + const note2 = await post(alice, { + text: 'baz2', + }); + + res = await api('/clips/add-note', { + clipId: clip1.id, + noteId: note1.id, + }, alice); + assert.strictEqual(res.status, 204); + + res = await api('/clips/add-note', { + clipId: clip2.id, + noteId: note2.id, + }, alice); + assert.strictEqual(res.status, 204); + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].name, 'kawaii'); + assert.strictEqual(exported[0].clipNotes.length, 1); + assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1'); + assert.strictEqual(exported[1].name, 'yuri'); + assert.strictEqual(exported[1].clipNotes.length, 1); + assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2'); + }); + + test('Clipping other user\'s note', async () => { + let res = await api('/clips/create', { + name: 'kawaii', + description: 'kawaii', + }, alice); + assert.strictEqual(res.status, 200); + const clip = res.body; + + const note = await post(bob, { + text: 'baz', + visibility: 'followers', + }); + + res = await api('/clips/add-note', { + clipId: clip.id, + noteId: note.id, + }, alice); + assert.strictEqual(res.status, 204); + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].name, 'kawaii'); + assert.strictEqual(exported[0].clipNotes.length, 1); + assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz'); + assert.strictEqual(exported[0].clipNotes[0].note.user.username, 'bob'); + }); +}); diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 251d662760..0d23b4fe67 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -6,9 +6,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { startServer, channel, clip, cookie, galleryPost, signup, page, play, post, simpleGet, uploadFile } from '../utils.js'; +import { channel, clip, cookie, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js'; import type { SimpleGetResponse } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; // Request Accept @@ -23,9 +22,7 @@ const HTML = 'text/html; charset=utf-8'; const JSON_UTF8 = 'application/json; charset=utf-8'; describe('Webリソース', () => { - let app: INestApplicationContext; - - let alice: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; let aliceUploadedFile: any; let alicesPost: any; let alicePage: any; @@ -34,7 +31,7 @@ describe('Webリソース', () => { let aliceGalleryPost: any; let aliceChannel: any; - let bob: misskey.entities.MeSignup; + let bob: misskey.entities.SignupResponse; type Request = { path: string, @@ -79,7 +76,6 @@ describe('Webリソース', () => { }; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); aliceUploadedFile = await uploadFile(alice); alicesPost = await post(alice, { @@ -96,10 +92,6 @@ describe('Webリソース', () => { bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - describe.each([ { path: '/', type: HTML }, { path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。" diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index 1fbd45c741..1fe0478a18 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -6,26 +6,18 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, startServer, simpleGet } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, signup, simpleGet } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('FF visibility', () => { - let app: INestApplicationContext; - - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { await api('/i/update', { followingVisibility: 'public', diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index b009ef124a..3937203569 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -3,35 +3,35 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { INestApplicationContext } from '@nestjs/common'; + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { loadConfig } from '@/config.js'; import { MiUser, UsersRepository } from '@/models/_.js'; -import { jobQueue } from '@/boot/common.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; -import { uploadFile, signup, startServer, initTestDb, api, sleep, successfulApiCall } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { jobQueue } from '@/boot/common.js'; +import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Account Move', () => { - let app: INestApplicationContext; let jq: INestApplicationContext; let url: URL; let root: any; - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; - let dave: misskey.entities.MeSignup; - let eve: misskey.entities.MeSignup; - let frank: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; + let dave: misskey.entities.SignupResponse; + let eve: misskey.entities.SignupResponse; + let frank: misskey.entities.SignupResponse; let Users: UsersRepository; beforeAll(async () => { - app = await startServer(); jq = await jobQueue(); + const config = loadConfig(); url = new URL(config.url); const connection = await initTestDb(false); @@ -46,7 +46,7 @@ describe('Account Move', () => { }, 1000 * 60 * 2); afterAll(async () => { - await Promise.all([app.close(), jq.close()]); + await jq.close(); }); describe('Create Alias', () => { diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts index a4b57a1eba..5144df5ebe 100644 --- a/packages/backend/test/e2e/mute.ts +++ b/packages/backend/test/e2e/mute.ts @@ -6,29 +6,21 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, react, startServer, waitFire } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, post, react, signup, waitFire } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Mute', () => { - let app: INestApplicationContext; - // alice mutes carol - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - test('ミュート作成', async () => { const res = await api('/mute/create', { userId: carol.id, diff --git a/packages/backend/test/e2e/nodeinfo.ts b/packages/backend/test/e2e/nodeinfo.ts index 7eed39c5ed..934ef08507 100644 --- a/packages/backend/test/e2e/nodeinfo.ts +++ b/packages/backend/test/e2e/nodeinfo.ts @@ -6,20 +6,9 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { relativeFetch, startServer } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { relativeFetch } from '../utils.js'; describe('nodeinfo', () => { - let app: INestApplicationContext; - - beforeAll(async () => { - app = await startServer(); - }, 1000 * 60 * 2); - - afterAll(async () => { - await app.close(); - }); - test('nodeinfo 2.1', async () => { const res = await relativeFetch('nodeinfo/2.1'); assert.ok(res.ok); diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 961df99cc2..0f2e08e675 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -8,29 +8,22 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { MiNote } from '@/models/Note.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, initTestDb, post, signup, uploadFile, uploadUrl } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Note', () => { - let app: INestApplicationContext; let Notes: any; - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; beforeAll(async () => { - app = await startServer(); const connection = await initTestDb(true); Notes = connection.getRepository(MiNote); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - test('投稿できる', async () => { const post = { text: 'test', diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index 3a5e4ebdae..df6ff42df9 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -11,13 +11,18 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { AuthorizationCode, ResourceOwnerPassword, type AuthorizationTokenConfig, ClientCredentials, ModuleOptions } from 'simple-oauth2'; +import { + AuthorizationCode, + type AuthorizationTokenConfig, + ClientCredentials, + ModuleOptions, + ResourceOwnerPassword, +} from 'simple-oauth2'; import pkceChallenge from 'pkce-challenge'; import { JSDOM } from 'jsdom'; -import Fastify, { type FastifyReply, type FastifyInstance } from 'fastify'; -import { api, port, signup, startServer } from '../utils.js'; +import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify'; +import { api, port, sendEnvUpdateRequest, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; -import type { INestApplicationContext } from '@nestjs/common'; const host = `http://127.0.0.1:${port}`; @@ -75,7 +80,7 @@ function getMeta(html: string): { transactionId: string | undefined, clientName: }; } -function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> { +function fetchDecision(transactionId: string, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise<Response> { return fetch(new URL('/oauth/decision', host), { method: 'post', body: new URLSearchParams({ @@ -90,14 +95,14 @@ function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, { }); } -async function fetchDecisionFromResponse(response: Response, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> { +async function fetchDecisionFromResponse(response: Response, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise<Response> { const { transactionId } = getMeta(await response.text()); assert.ok(transactionId); return await fetchDecision(transactionId, user, { cancel }); } -async function fetchAuthorizationCode(user: misskey.entities.MeSignup, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> { +async function fetchAuthorizationCode(user: misskey.entities.SignupResponse, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> { const client = new AuthorizationCode(clientConfig); const response = await fetch(client.authorizeURL({ @@ -147,16 +152,14 @@ async function assertDirectError(response: Response, status: number, error: stri } describe('OAuth', () => { - let app: INestApplicationContext; let fastify: FastifyInstance; - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; let sender: (reply: FastifyReply) => void; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); @@ -168,7 +171,7 @@ describe('OAuth', () => { }, 1000 * 60 * 2); beforeEach(async () => { - process.env.MISSKEY_TEST_CHECK_IP_RANGE = ''; + await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '' }); sender = (reply): void => { reply.send(` <!DOCTYPE html> @@ -180,7 +183,6 @@ describe('OAuth', () => { afterAll(async () => { await fastify.close(); - await app.close(); }); test('Full flow', async () => { @@ -881,7 +883,7 @@ describe('OAuth', () => { }); test('Disallow loopback', async () => { - process.env.MISSKEY_TEST_CHECK_IP_RANGE = '1'; + await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '1' }); const client = new AuthorizationCode(clientConfig); const response = await fetch(client.authorizeURL({ diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts index 7d57ba17b6..42cc414c3f 100644 --- a/packages/backend/test/e2e/renote-mute.ts +++ b/packages/backend/test/e2e/renote-mute.ts @@ -6,29 +6,21 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, react, startServer, waitFire, sleep } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, post, signup, sleep, waitFire } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Renote Mute', () => { - let app: INestApplicationContext; - // alice mutes carol - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - test('ミュート作成', async () => { const res = await api('/renote-mute/create', { userId: carol.id, diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index 288c54bdbc..b6f584fa70 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -8,12 +8,10 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { WebSocket } from 'ws'; import { MiFollowing } from '@/models/Following.js'; -import { signup, api, post, startServer, initTestDb, waitFire, createAppToken, port } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Streaming', () => { - let app: INestApplicationContext; let Followings: any; const follow = async (follower: any, followee: any) => { @@ -32,15 +30,15 @@ describe('Streaming', () => { describe('Streaming', () => { // Local users - let ayano: misskey.entities.MeSignup; - let kyoko: misskey.entities.MeSignup; - let chitose: misskey.entities.MeSignup; - let kanako: misskey.entities.MeSignup; + let ayano: misskey.entities.SignupResponse; + let kyoko: misskey.entities.SignupResponse; + let chitose: misskey.entities.SignupResponse; + let kanako: misskey.entities.SignupResponse; // Remote users - let akari: misskey.entities.MeSignup; - let chinatsu: misskey.entities.MeSignup; - let takumi: misskey.entities.MeSignup; + let akari: misskey.entities.SignupResponse; + let chinatsu: misskey.entities.SignupResponse; + let takumi: misskey.entities.SignupResponse; let kyokoNote: any; let kanakoNote: any; @@ -48,7 +46,6 @@ describe('Streaming', () => { let list: any; beforeAll(async () => { - app = await startServer(); const connection = await initTestDb(true); Followings = connection.getRepository(MiFollowing); @@ -95,10 +92,6 @@ describe('Streaming', () => { }, chitose); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - describe('Events', () => { test('mention event', async () => { const fired = await waitFire( diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts index 0e487976dc..26c30d6c4c 100644 --- a/packages/backend/test/e2e/thread-mute.ts +++ b/packages/backend/test/e2e/thread-mute.ts @@ -6,28 +6,20 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, connectStream, startServer } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, connectStream, post, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Note thread mute', () => { - let app: INestApplicationContext; - - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => { const bobNote = await post(bob, { text: '@alice @carol root note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index cb9558b416..88f89c4a6f 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -6,12 +6,8 @@ // How to run: // pnpm jest -- e2e/timelines.ts -process.env.NODE_ENV = 'test'; -process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true'; - import * as assert from 'assert'; -import { api, post, randomString, signup, sleep, startServer, uploadUrl } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, post, randomString, sendEnvUpdateRequest, signup, sleep, uploadUrl } from '../utils.js'; function genHost() { return randomString() + '.example.com'; @@ -21,16 +17,6 @@ function waitForPushToTl() { return sleep(500); } -let app: INestApplicationContext; - -beforeAll(async () => { - app = await startServer(); -}, 1000 * 60 * 2); - -afterAll(async () => { - await app.close(); -}); - describe('Timelines', () => { describe('Home TL', () => { test.concurrent('自分の visibility: followers なノートが含まれる', async () => { @@ -334,8 +320,9 @@ describe('Timelines', () => { test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); await api('/following/create', { userId: bob.id }, alice); - await sleep(1000); + const bobNote = await post(bob, { text: 'hi' }); await waitForPushToTl(); @@ -348,8 +335,9 @@ describe('Timelines', () => { test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); await api('/following/create', { userId: bob.id }, alice); - await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); @@ -762,8 +750,9 @@ describe('Timelines', () => { test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); await api('/following/create', { userId: bob.id }, alice); - await sleep(1000); + const bobNote = await post(bob, { text: 'hi' }); await waitForPushToTl(); @@ -776,8 +765,9 @@ describe('Timelines', () => { test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); await api('/following/create', { userId: bob.id }, alice); - await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts index b5f00a6327..07da0db369 100644 --- a/packages/backend/test/e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -6,20 +6,16 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, uploadUrl, startServer } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, post, signup, uploadUrl } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('users/notes', () => { - let app: INestApplicationContext; - - let alice: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; let jpgNote: any; let pngNote: any; let jpgPngNote: any; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); @@ -34,10 +30,6 @@ describe('users/notes', () => { }); }, 1000 * 60 * 2); - afterAll(async() => { - await app.close(); - }); - test('withFiles', async () => { const res = await api('/users/notes', { userId: alice.id, diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index be6f0ec855..572674d81e 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -8,20 +8,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { inspect } from 'node:util'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; -import type { Packed } from '@/misc/json-schema.js'; -import { - signup, - post, - page, - role, - startServer, - api, - successfulApiCall, - failedApiCall, - uploadFile, -} from '../utils.js'; +import { api, page, post, role, signup, successfulApiCall, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; -import type { INestApplicationContext } from '@nestjs/common'; describe('ユーザー', () => { // エンティティとしてのユーザーを主眼においたテストを記述する @@ -188,8 +176,6 @@ describe('ユーザー', () => { }); }; - let app: INestApplicationContext; - let root: User; let alice: User; let aliceNote: misskey.entities.Note; @@ -234,10 +220,6 @@ describe('ユーザー', () => { let userFollowRequested: User; beforeAll(async () => { - app = await startServer(); - }, 1000 * 60 * 2); - - beforeAll(async () => { root = await signup({ username: 'root' }); alice = await signup({ username: 'alice' }); aliceNote = await post(alice, { text: 'test' }) as any; @@ -324,10 +306,6 @@ describe('ユーザー', () => { await api('following/create', { userId: userFollowRequested.id }, userFollowRequesting); }, 1000 * 60 * 10); - afterAll(async () => { - await app.close(); - }); - beforeEach(async () => { alice = { ...alice, diff --git a/packages/backend/test/e2e/well-known.ts b/packages/backend/test/e2e/well-known.ts index 14e32e1627..0429b7c8b2 100644 --- a/packages/backend/test/e2e/well-known.ts +++ b/packages/backend/test/e2e/well-known.ts @@ -6,24 +6,16 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { host, origin, relativeFetch, signup, startServer } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { host, origin, relativeFetch, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('.well-known', () => { - let app: INestApplicationContext; let alice: misskey.entities.User; beforeAll(async () => { - app = await startServer(); - alice = await signup({ username: 'alice' }); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - test('nodeinfo', async () => { const res = await relativeFetch('.well-known/nodeinfo'); assert.ok(res.ok); diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts new file mode 100644 index 0000000000..cf5b9bf24d --- /dev/null +++ b/packages/backend/test/jest.setup.ts @@ -0,0 +1,8 @@ +import { initTestDb, sendEnvResetRequest } from './utils.js'; + +beforeAll(async () => { + await Promise.all([ + initTestDb(false), + sendEnvResetRequest(), + ]); +}); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 7cba7a2aa8..7ee65d1ab0 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -15,7 +15,13 @@ import type { LoggerService } from '@/core/LoggerService.js'; import type { MetaService } from '@/core/MetaService.js'; import type { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; -import type { NoteReactionsRepository, NotesRepository, PollsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; +import type { + FollowRequestsRepository, + NoteReactionsRepository, + NotesRepository, + PollsRepository, + UsersRepository, +} from '@/models/_.js'; type MockResponse = { type: string; diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts index f2aa5d35e4..f02c4e6700 100644 --- a/packages/backend/test/unit/AnnouncementService.ts +++ b/packages/backend/test/unit/AnnouncementService.ts @@ -10,7 +10,13 @@ import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import { GlobalModule } from '@/GlobalModule.js'; import { AnnouncementService } from '@/core/AnnouncementService.js'; -import type { MiAnnouncement, AnnouncementsRepository, AnnouncementReadsRepository, UsersRepository, MiUser } from '@/models/_.js'; +import type { + AnnouncementReadsRepository, + AnnouncementsRepository, + MiAnnouncement, + MiUser, + UsersRepository, +} from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { genAidx } from '@/misc/id/aidx.js'; import { CacheService } from '@/core/CacheService.js'; diff --git a/packages/backend/test/unit/DriveService.ts b/packages/backend/test/unit/DriveService.ts index 7234da2e36..64397a1a4f 100644 --- a/packages/backend/test/unit/DriveService.ts +++ b/packages/backend/test/unit/DriveService.ts @@ -6,7 +6,13 @@ process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; -import { DeleteObjectCommandOutput, DeleteObjectCommand, NoSuchKey, InvalidObjectState, S3Client } from '@aws-sdk/client-s3'; +import { + DeleteObjectCommand, + DeleteObjectCommandOutput, + InvalidObjectState, + NoSuchKey, + S3Client, +} from '@aws-sdk/client-s3'; import { mockClient } from 'aws-sdk-client-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { DriveService } from '@/core/DriveService.js'; diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts index 34200899d4..cddc374f9a 100644 --- a/packages/backend/test/unit/FetchInstanceMetadataService.ts +++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts @@ -55,7 +55,8 @@ describe('FetchInstanceMetadataService', () => { return { fetch: jest.fn() }; } else if (token === DI.redis) { return mockRedis; - }}) + } + }) .compile(); app.enableShutdownHooks(); diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts index ba524adff4..8604db2eea 100644 --- a/packages/backend/test/unit/FileInfoService.ts +++ b/packages/backend/test/unit/FileInfoService.ts @@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; -import { describe, beforeAll, afterAll, test } from '@jest/globals'; +import { afterAll, beforeAll, describe, test } from '@jest/globals'; import { GlobalModule } from '@/GlobalModule.js'; import { FileInfoService } from '@/core/FileInfoService.js'; //import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/test/unit/MetaService.ts b/packages/backend/test/unit/MetaService.ts index ab30f48283..c4c7f21913 100644 --- a/packages/backend/test/unit/MetaService.ts +++ b/packages/backend/test/unit/MetaService.ts @@ -6,15 +6,13 @@ process.env.NODE_ENV = 'test'; import { jest } from '@jest/globals'; -import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import { GlobalModule } from '@/GlobalModule.js'; -import type { MetasRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { CoreModule } from '@/core/CoreModule.js'; -import type { DataSource } from 'typeorm'; import type { TestingModule } from '@nestjs/testing'; +import type { DataSource } from 'typeorm'; describe('MetaService', () => { let app: TestingModule; diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 9879eb8e3e..46613c29c8 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -11,7 +11,7 @@ import { Test } from '@nestjs/testing'; import * as lolex from '@sinonjs/fake-timers'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; -import type { MiRole, RolesRepository, RoleAssignmentsRepository, UsersRepository, MiUser } from '@/models/_.js'; +import type { MiRole, MiUser, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { genAidx } from '@/misc/id/aidx.js'; diff --git a/packages/backend/test/unit/S3Service.ts b/packages/backend/test/unit/S3Service.ts index c1eafc96b7..2ffc99380d 100644 --- a/packages/backend/test/unit/S3Service.ts +++ b/packages/backend/test/unit/S3Service.ts @@ -6,7 +6,13 @@ process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; -import { UploadPartCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import { + CompleteMultipartUploadCommand, + CreateMultipartUploadCommand, + PutObjectCommand, + S3Client, + UploadPartCommand, +} from '@aws-sdk/client-s3'; import { mockClient } from 'aws-sdk-client-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; diff --git a/packages/backend/test/unit/misc/id.ts b/packages/backend/test/unit/misc/id.ts index 59783a9fa1..1498c075aa 100644 --- a/packages/backend/test/unit/misc/id.ts +++ b/packages/backend/test/unit/misc/id.ts @@ -4,13 +4,13 @@ */ import { ulid } from 'ulid'; -import { describe, test, expect } from '@jest/globals'; +import { describe, expect, test } from '@jest/globals'; import { aidRegExp, genAid, parseAid } from '@/misc/id/aid.js'; import { aidxRegExp, genAidx, parseAidx } from '@/misc/id/aidx.js'; import { genMeid, meidRegExp, parseMeid } from '@/misc/id/meid.js'; import { genMeidg, meidgRegExp, parseMeidg } from '@/misc/id/meidg.js'; import { genObjectId, objectIdRegExp, parseObjectId } from '@/misc/id/object-id.js'; -import { ulidRegExp, parseUlid } from '@/misc/id/ulid.js'; +import { parseUlid, ulidRegExp } from '@/misc/id/ulid.js'; describe('misc:id', () => { test('aid', () => { diff --git a/packages/backend/test/unit/misc/others.ts b/packages/backend/test/unit/misc/others.ts index b16d26d866..caa815b3df 100644 --- a/packages/backend/test/unit/misc/others.ts +++ b/packages/backend/test/unit/misc/others.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { describe, test, expect } from '@jest/globals'; +import { describe, expect, test } from '@jest/globals'; import { contentDisposition } from '@/misc/content-disposition.js'; describe('misc:content-disposition', () => { diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 46b8ea9cdd..2b232a0a5d 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -5,7 +5,7 @@ import * as assert from 'node:assert'; import { readFile } from 'node:fs/promises'; -import { isAbsolute, basename } from 'node:path'; +import { basename, isAbsolute } from 'node:path'; import { randomUUID } from 'node:crypto'; import { inspect } from 'node:util'; import WebSocket, { ClientOptions } from 'ws'; @@ -17,7 +17,7 @@ import { entities } from '../src/postgres.js'; import { loadConfig } from '../src/config.js'; import type * as misskey from 'misskey-js'; -export { server as startServer } from '@/boot/common.js'; +export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; interface UserToken { token: string; @@ -68,7 +68,11 @@ export const failedApiCall = async <T, >(request: ApiRequest, assertion: { return res.body; }; -const request = async (path: string, params: any, me?: UserToken): Promise<{ status: number, headers: Headers, body: any }> => { +const request = async (path: string, params: any, me?: UserToken): Promise<{ + status: number, + headers: Headers, + body: any +}> => { const bodyAuth: Record<string, string> = {}; const headers: Record<string, string> = { 'Content-Type': 'application/json', @@ -275,7 +279,11 @@ interface UploadOptions { * Upload file * @param user User */ -export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ status: number, headers: Headers, body: misskey.Endpoints['drive/files/create']['res'] | null }> => { +export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ + status: number, + headers: Headers, + body: misskey.Endpoints['drive/files/create']['res'] | null +}> => { const absPath = path == null ? new URL('resources/Lenna.jpg', import.meta.url) : isAbsolute(path.toString()) @@ -426,8 +434,8 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde ]; const body = - jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : - htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : + jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : + htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : null; return { @@ -557,3 +565,34 @@ export function sleep(msec: number) { }, msec); }); } + +export async function sendEnvUpdateRequest(params: { key: string, value?: string }) { + const res = await fetch( + `http://localhost:${port + 1000}/env`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }, + ); + + if (res.status !== 200) { + throw new Error('server env update failed.'); + } +} + +export async function sendEnvResetRequest() { + const res = await fetch( + `http://localhost:${port + 1000}/env-reset`, + { + method: 'POST', + body: JSON.stringify({}), + }, + ); + + if (res.status !== 200) { + throw new Error('server env update failed.'); + } +} diff --git a/packages/frontend/assets/drop-and-fusion/bgm_1.mp3 b/packages/frontend/assets/drop-and-fusion/bgm_1.mp3 Binary files differnew file mode 100644 index 0000000000..cafc34ad9c --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/bgm_1.mp3 diff --git a/packages/frontend/assets/drop-and-fusion/bubble2.mp3 b/packages/frontend/assets/drop-and-fusion/bubble2.mp3 Binary files differnew file mode 100644 index 0000000000..8b4f8df6e9 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/bubble2.mp3 diff --git a/packages/frontend/assets/drop-and-fusion/cold_face.png b/packages/frontend/assets/drop-and-fusion/cold_face.png Binary files differnew file mode 100644 index 0000000000..f5f53e9efc --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/cold_face.png diff --git a/packages/frontend/assets/drop-and-fusion/drop-arrow.svg b/packages/frontend/assets/drop-and-fusion/drop-arrow.svg new file mode 100644 index 0000000000..f98bb8a1ac --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/drop-arrow.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg width="100%" height="100%" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> + <path d="M0,0L128,0L64,64L0,0Z" style="fill:rgb(255,61,0);"/> + <path d="M0,0L128,0L64,64L0,0ZM28.971,12L64,47.029C64,47.029 99.029,12 99.029,12L28.971,12Z" style="fill:rgb(255,122,0);"/> +</svg> diff --git a/packages/frontend/assets/drop-and-fusion/dropper.png b/packages/frontend/assets/drop-and-fusion/dropper.png Binary files differnew file mode 100644 index 0000000000..f4300aa5c0 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/dropper.png diff --git a/packages/frontend/assets/drop-and-fusion/exploding_head.png b/packages/frontend/assets/drop-and-fusion/exploding_head.png Binary files differnew file mode 100644 index 0000000000..e8ec5182c8 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/exploding_head.png diff --git a/packages/frontend/assets/drop-and-fusion/face_with_open_mouth.png b/packages/frontend/assets/drop-and-fusion/face_with_open_mouth.png Binary files differnew file mode 100644 index 0000000000..c523020f62 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/face_with_open_mouth.png diff --git a/packages/frontend/assets/drop-and-fusion/face_with_symbols_on_mouth.png b/packages/frontend/assets/drop-and-fusion/face_with_symbols_on_mouth.png Binary files differnew file mode 100644 index 0000000000..db9e839c84 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/face_with_symbols_on_mouth.png diff --git a/packages/frontend/assets/drop-and-fusion/frame-dark.svg b/packages/frontend/assets/drop-and-fusion/frame-dark.svg new file mode 100644 index 0000000000..3fa7c0da81 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/frame-dark.svg @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg width="100%" height="100%" viewBox="0 0 450 600" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"> + <g> + <g transform="matrix(0.944444,0,0,0.8125,12.5,100)"> + <rect x="0" y="0" width="450" height="600"/> + </g> + <g transform="matrix(0.944444,0,0,0.8125,12.5,100)"> + <rect x="0" y="0" width="450" height="600" style="fill:rgb(255,147,2);fill-opacity:0.15;"/> + </g> + <use xlink:href="#_Image1" x="0" y="49.048" width="450px" height="551px"/> + </g> + <g transform="matrix(0.755719,0.654896,-0.654896,0.755719,383.517,-217.265)"> + <g transform="matrix(0.755719,-0.654896,0.654896,0.755719,-147.545,415.355)"> + <use xlink:href="#_Image2" x="0" y="49" width="450px" height="551px"/> + </g> + </g> + <use xlink:href="#_Image3" x="25" y="99.5" width="400px" height="475px"/> + <g transform="matrix(1,0,0,2,1.13687e-13,25)"> + <rect x="25" y="37.5" width="400" height="12.5" style="fill:url(#_Linear4);"/> + </g> + <defs> + <image id="_Image1" width="450px" height="551px" xlink:href="data:image/png;base64,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CSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCp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"/> + <image id="_Image2" width="450px" height="551px" xlink:href="data:image/png;base64,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"/> + <image id="_Image3" width="400px" height="475px" xlink:href="data:image/png;base64,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"/> + <linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(7.65404e-16,12.5,-0.390625,2.39189e-17,225,37.5)"><stop offset="0" style="stop-color:rgb(255,14,0);stop-opacity:0.5"/><stop offset="1" style="stop-color:rgb(255,13,0);stop-opacity:0"/></linearGradient> + </defs> +</svg> diff --git a/packages/frontend/assets/drop-and-fusion/frame-light.svg b/packages/frontend/assets/drop-and-fusion/frame-light.svg new file mode 100644 index 0000000000..6052ccbaa0 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/frame-light.svg @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg width="100%" height="100%" viewBox="0 0 450 600" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"> + <g> + <g transform="matrix(0.944444,0,0,0.8125,12.5,100)"> + <rect x="0" y="0" width="450" height="600" style="fill:white;"/> + </g> + <g transform="matrix(0.944444,0,0,0.8125,12.5,100)"> + <rect x="0" y="0" width="450" height="600" style="fill:rgb(255,147,2);fill-opacity:0.15;"/> + </g> + <use xlink:href="#_Image1" x="0" y="49.048" width="450px" height="551px"/> + </g> + <g transform="matrix(0.755719,0.654896,-0.654896,0.755719,383.517,-217.265)"> + <g transform="matrix(0.755719,-0.654896,0.654896,0.755719,-147.545,415.355)"> + <use xlink:href="#_Image2" x="0" y="49" width="450px" height="551px"/> + </g> + </g> + <use xlink:href="#_Image3" x="25" y="99.5" width="400px" height="475px"/> + <g transform="matrix(1,0,0,2,1.13687e-13,25)"> + <rect x="25" y="37.5" width="400" height="12.5" style="fill:url(#_Linear4);"/> + </g> + <defs> + <image id="_Image1" width="450px" height="551px" xlink:href="data:image/png;base64,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CSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCp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"/> + <image id="_Image2" width="450px" height="551px" xlink:href="data:image/png;base64,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"/> + <image id="_Image3" width="400px" height="475px" xlink:href="data:image/png;base64,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"/> + <linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(7.65404e-16,12.5,-0.390625,2.39189e-17,225,37.5)"><stop offset="0" style="stop-color:rgb(255,14,0);stop-opacity:0.5"/><stop offset="1" style="stop-color:rgb(255,13,0);stop-opacity:0"/></linearGradient> + </defs> +</svg> diff --git a/packages/frontend/assets/drop-and-fusion/gameover.png b/packages/frontend/assets/drop-and-fusion/gameover.png Binary files differnew file mode 100644 index 0000000000..8b622577ca --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/gameover.png diff --git a/packages/frontend/assets/drop-and-fusion/grinning_squinting_face.png b/packages/frontend/assets/drop-and-fusion/grinning_squinting_face.png Binary files differnew file mode 100644 index 0000000000..fd72d749a1 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/grinning_squinting_face.png diff --git a/packages/frontend/assets/drop-and-fusion/heart_suit.png b/packages/frontend/assets/drop-and-fusion/heart_suit.png Binary files differnew file mode 100644 index 0000000000..b0105f8582 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/heart_suit.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_1.png b/packages/frontend/assets/drop-and-fusion/keycap_1.png Binary files differnew file mode 100644 index 0000000000..d672f2854a --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/keycap_1.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_10.png b/packages/frontend/assets/drop-and-fusion/keycap_10.png Binary files differnew file mode 100644 index 0000000000..32cf193540 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/keycap_10.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_2.png b/packages/frontend/assets/drop-and-fusion/keycap_2.png Binary files differnew file mode 100644 index 0000000000..81c3f58e6e --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/keycap_2.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_3.png b/packages/frontend/assets/drop-and-fusion/keycap_3.png Binary files differnew file mode 100644 index 0000000000..424d8c123d --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/keycap_3.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_4.png b/packages/frontend/assets/drop-and-fusion/keycap_4.png Binary files differnew file mode 100644 index 0000000000..ea6ae50531 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/keycap_4.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_5.png b/packages/frontend/assets/drop-and-fusion/keycap_5.png Binary files differnew file mode 100644 index 0000000000..ad435da69a --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/keycap_5.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_6.png b/packages/frontend/assets/drop-and-fusion/keycap_6.png Binary files differnew file mode 100644 index 0000000000..70c9522b43 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/keycap_6.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_7.png b/packages/frontend/assets/drop-and-fusion/keycap_7.png Binary files differnew file mode 100644 index 0000000000..5a24307487 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/keycap_7.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_8.png b/packages/frontend/assets/drop-and-fusion/keycap_8.png Binary files differnew file mode 100644 index 0000000000..9689d8ecfb --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/keycap_8.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_9.png b/packages/frontend/assets/drop-and-fusion/keycap_9.png Binary files differnew file mode 100644 index 0000000000..ac3f638841 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/keycap_9.png diff --git a/packages/frontend/assets/drop-and-fusion/logo.png b/packages/frontend/assets/drop-and-fusion/logo.png Binary files differnew file mode 100644 index 0000000000..c6725bea88 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/logo.png diff --git a/packages/frontend/assets/drop-and-fusion/pleading_face.png b/packages/frontend/assets/drop-and-fusion/pleading_face.png Binary files differnew file mode 100644 index 0000000000..42f58d411c --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/pleading_face.png diff --git a/packages/frontend/assets/drop-and-fusion/poi1.mp3 b/packages/frontend/assets/drop-and-fusion/poi1.mp3 Binary files differnew file mode 100644 index 0000000000..59dae90965 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/poi1.mp3 diff --git a/packages/frontend/assets/drop-and-fusion/poi2.mp3 b/packages/frontend/assets/drop-and-fusion/poi2.mp3 Binary files differnew file mode 100644 index 0000000000..a65c653891 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/poi2.mp3 diff --git a/packages/frontend/assets/drop-and-fusion/smiling_face_with_hearts.png b/packages/frontend/assets/drop-and-fusion/smiling_face_with_hearts.png Binary files differnew file mode 100644 index 0000000000..416ef0410a --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/smiling_face_with_hearts.png diff --git a/packages/frontend/assets/drop-and-fusion/smiling_face_with_sunglasses.png b/packages/frontend/assets/drop-and-fusion/smiling_face_with_sunglasses.png Binary files differnew file mode 100644 index 0000000000..c0f72254c2 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/smiling_face_with_sunglasses.png diff --git a/packages/frontend/assets/drop-and-fusion/zany_face.png b/packages/frontend/assets/drop-and-fusion/zany_face.png Binary files differnew file mode 100644 index 0000000000..f14f9db20b --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/zany_face.png diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 68aa501c84..6230f49e72 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "watch": "vite", - "dev": "vite --config vite.config.local-dev.ts", + "dev": "vite --config vite.config.local-dev.ts --debug hmr", "build": "vite build", "storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"", "build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", @@ -19,6 +19,8 @@ "dependencies": { "@discordapp/twemoji": "15.0.2", "@github/webauthn-json": "2.1.1", + "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", + "@misskey-dev/browser-image-resizer": "2.2.1-misskey.10", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "5.0.5", "@rollup/pluginutils": "5.1.0", @@ -26,12 +28,11 @@ "@syuilo/aiscript": "0.16.0", "@phosphor-icons/web": "^2.0.3", "@twemoji/parser": "15.0.0", - "@vitejs/plugin-vue": "4.5.2", - "@vue/compiler-sfc": "3.3.12", + "@vitejs/plugin-vue": "5.0.2", + "@vue/compiler-sfc": "3.4.3", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6", "astring": "1.8.6", "broadcast-channel": "7.0.0", - "browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "buraha": "0.0.1", "canvas-confetti": "1.6.1", "chart.js": "4.4.1", @@ -46,7 +47,6 @@ "escape-regexp": "0.0.1", "estree-walker": "3.0.3", "eventemitter3": "5.0.1", - "gsap": "3.12.4", "idb-keyval": "6.2.1", "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", @@ -71,10 +71,12 @@ "uuid": "9.0.1", "v-code-diff": "1.7.2", "vite": "5.0.10", - "vue": "3.3.12", + "vue": "3.4.3", "vuedraggable": "next" }, "devDependencies": { + "@misskey-dev/eslint-plugin": "^1.0.0", + "@misskey-dev/summaly": "^5.0.3", "@storybook/addon-actions": "7.6.5", "@storybook/addon-essentials": "7.6.5", "@storybook/addon-interactions": "7.6.5", @@ -108,7 +110,7 @@ "@typescript-eslint/eslint-plugin": "6.14.0", "@typescript-eslint/parser": "6.14.0", "@vitest/coverage-v8": "0.34.6", - "@vue/runtime-core": "3.3.12", + "@vue/runtime-core": "3.4.3", "acorn": "8.11.2", "cross-env": "7.0.3", "cypress": "13.6.1", @@ -128,11 +130,10 @@ "start-server-and-test": "2.0.3", "storybook": "7.6.5", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", - "summaly": "github:misskey-dev/summaly", "vite-plugin-turbosnap": "1.0.3", "vitest": "0.34.6", "vitest-fetch-mock": "0.2.2", "vue-eslint-parser": "9.3.2", - "vue-tsc": "1.8.25" + "vue-tsc": "1.8.27" } } diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 05008194f0..de0a2da48b 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -11,7 +11,8 @@ import { miLocalStorage } from '@/local-storage.js'; import { MenuButton } from '@/types/menu.js'; import { del, get, set } from '@/scripts/idb-proxy.js'; import { apiUrl } from '@/config.js'; -import { waiting, api, popup, popupMenu, success, alert } from '@/os.js'; +import { waiting, popup, popupMenu, success, alert } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js'; // TODO: 他のタブと永続化されたstateを同期 @@ -23,9 +24,14 @@ const accountData = miLocalStorage.getItem('account'); // TODO: 外部からはreadonlyに export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; -export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); +export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true); export const iAmAdmin = $i != null && $i.isAdmin; +export function signinRequired() { + if ($i == null) throw new Error('signin required'); + return $i; +} + export let notesCount = $i == null ? 0 : $i.notesCount; export function incNotesCount() { notesCount++; @@ -246,7 +252,7 @@ export async function openAccountMenu(opts: { } const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id)); - const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) }); + const accountsPromise = misskeyApi('users/show', { userIds: storedAccounts.map(x => x.id) }); function createItem(account: Misskey.entities.UserDetailed) { return { diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 63f169d9ad..2d1d0b8011 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -22,6 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; +import { setupRouter } from '@/global/router/definition.js'; export async function common(createVue: () => App<Element>) { console.info(`Sharkey v${version}`); @@ -245,6 +246,8 @@ export async function common(createVue: () => App<Element>) { const app = createVue(); + setupRouter(app); + if (_DEV_) { app.config.performance = true; } diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index cdc1d11ca2..81e0f3ae92 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -3,23 +3,23 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { createApp, markRaw, defineAsyncComponent } from 'vue'; +import { createApp, defineAsyncComponent, markRaw } from 'vue'; import { common } from './common.js'; import { ui } from '@/config.js'; import { i18n } from '@/i18n.js'; -import { confirm, alert, post, popup, toast } from '@/os.js'; +import { alert, confirm, popup, post, toast } from '@/os.js'; import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; -import { $i, updateAccount, signout } from '@/account.js'; -import { defaultStore, ColdDeviceStorage } from '@/store.js'; +import { $i, signout, updateAccount } from '@/account.js'; +import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { makeHotkey } from '@/scripts/hotkey.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; -import { mainRouter } from '@/router.js'; import { initializeSw } from '@/scripts/initialize-sw.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; +import { mainRouter } from '@/global/router/main.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts index 25d2b3c15f..20950add80 100644 --- a/packages/frontend/src/cache.ts +++ b/packages/frontend/src/cache.ts @@ -5,9 +5,9 @@ import * as Misskey from 'misskey-js'; import { Cache } from '@/scripts/cache.js'; -import { api } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; -export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => api('clips/list')); -export const rolesCache = new Cache(1000 * 60 * 30, () => api('admin/roles/list')); -export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => api('users/lists/list')); -export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => api('antennas/list')); +export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list')); +export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list')); +export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => misskeyApi('users/lists/list')); +export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list')); diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue index b11cf1c8a0..d0d67661fb 100644 --- a/packages/frontend/src/components/MkAccountMoved.vue +++ b/packages/frontend/src/components/MkAccountMoved.vue @@ -17,7 +17,7 @@ import * as Misskey from 'misskey-js'; import MkMention from './MkMention.vue'; import { i18n } from '@/i18n.js'; import { host as localHost } from '@/config.js'; -import { api } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const user = ref<Misskey.entities.UserLite>(); @@ -25,7 +25,7 @@ const props = defineProps<{ movedTo: string; // user id }>(); -api('users/show', { userId: props.movedTo }).then(u => user.value = u); +misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue index 333f62e29c..9a7078c2f9 100644 --- a/packages/frontend/src/components/MkAchievements.vue +++ b/packages/frontend/src/components/MkAchievements.vue @@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only import * as Misskey from 'misskey-js'; import { onMounted, ref, computed } from 'vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js'; @@ -71,7 +72,7 @@ const achievements = ref<Misskey.entities.UsersAchievementsResponse | null>(null const lockedAchievements = computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements.value ?? []).some(a => a.name === x))); function fetch() { - os.api('users/achievements', { userId: props.user.id }).then(res => { + misskeyApi('users/achievements', { userId: props.user.id }).then(res => { achievements.value = []; for (const t of ACHIEVEMENT_TYPES) { const a = res.find(x => x.name === t); diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue index 4c6e3e693a..eca5daf1c1 100644 --- a/packages/frontend/src/components/MkAnnouncementDialog.vue +++ b/packages/frontend/src/components/MkAnnouncementDialog.vue @@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkModal from '@/components/MkModal.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; @@ -49,7 +50,7 @@ async function ok() { } modal.value.close(); - os.api('i/read-announcement', { announcementId: props.announcement.id }); + misskeyApi('i/read-announcement', { announcementId: props.announcement.id }); updateAccount({ unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id), }); diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 6fd42f1eea..9ec44a9561 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -45,6 +45,7 @@ import contains from '@/scripts/contains.js'; import { char2twemojiFilePath, char2fluentEmojiFilePath, char2tossfaceFilePath } from '@/scripts/emoji-base.js'; import { acct } from '@/filters/user.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { emojilist, getEmojiName } from '@/scripts/emojilist.js'; import { i18n } from '@/i18n.js'; @@ -201,7 +202,7 @@ function exec() { users.value = JSON.parse(cache); fetching.value = false; } else { - os.api('users/search-by-username-and-host', { + misskeyApi('users/search-by-username-and-host', { username: props.q, limit: 10, detail: false, @@ -224,7 +225,7 @@ function exec() { hashtags.value = hashtags; fetching.value = false; } else { - os.api('hashtags/search', { + misskeyApi('hashtags/search', { query: props.q, limit: 30, }).then(searchedHashtags => { diff --git a/packages/frontend/src/components/MkAvatars.vue b/packages/frontend/src/components/MkAvatars.vue index 5644a324cf..6ef4a7dfe2 100644 --- a/packages/frontend/src/components/MkAvatars.vue +++ b/packages/frontend/src/components/MkAvatars.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const props = withDefaults(defineProps<{ userIds: string[]; @@ -27,7 +27,7 @@ const props = withDefaults(defineProps<{ const users = ref<Misskey.entities.UserLite[]>([]); onMounted(async () => { - users.value = await os.api('users/show', { + users.value = await misskeyApi('users/show', { userIds: props.userIds, }) as unknown as Misskey.entities.UserLite[]; }); diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index 9fcc49d3f0..da13f8a067 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -131,6 +131,10 @@ function onMousedown(evt: MouseEvent): void { box-sizing: border-box; transition: background 0.1s ease; + &:hover { + text-decoration: none; + } + &:not(:disabled):hover { background: var(--buttonHoverBg); } diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index 40bca11e64..f60c721eae 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -6,12 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> <span v-if="!available">{{ i18n.ts.waiting }}<MkEllipsis/></span> - <div ref="captchaEl"></div> + <div v-if="props.provider == 'mcaptcha'"> + <div id="mcaptcha__widget-container" class="m-captcha-style"></div> + <div ref="captchaEl"></div> + </div> + <div v-else ref="captchaEl"></div> </div> </template> <script lang="ts" setup> -import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch } from 'vue'; +import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; @@ -26,7 +30,7 @@ export type Captcha = { getResponse(id: string): string; }; -export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile'; +export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha'; type CaptchaContainer = { readonly [_ in CaptchaProvider]?: Captcha; @@ -39,6 +43,7 @@ declare global { const props = defineProps<{ provider: CaptchaProvider; sitekey: string | null; // null will show error on request + instanceUrl?: string | null; modelValue?: string | null; }>(); @@ -55,6 +60,7 @@ const variable = computed(() => { case 'hcaptcha': return 'hcaptcha'; case 'recaptcha': return 'grecaptcha'; case 'turnstile': return 'turnstile'; + case 'mcaptcha': return 'mcaptcha'; } }); @@ -65,6 +71,7 @@ const src = computed(() => { case 'hcaptcha': return 'https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off'; case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit'; case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; + case 'mcaptcha': return null; } }); @@ -72,9 +79,9 @@ const scriptId = computed(() => `script-${props.provider}`); const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha); -if (loaded) { +if (loaded || props.provider === 'mcaptcha') { available.value = true; -} else { +} else if (src.value !== null) { (document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), { async: true, id: scriptId.value, @@ -87,7 +94,7 @@ function reset() { if (captcha.value.reset) captcha.value.reset(); } -function requestRender() { +async function requestRender() { if (captcha.value.render && captchaEl.value instanceof Element) { captcha.value.render(captchaEl.value, { sitekey: props.sitekey, @@ -96,6 +103,15 @@ function requestRender() { 'expired-callback': callback, 'error-callback': callback, }); + } else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) { + const { default: Widget } = await import('@mcaptcha/vanilla-glue'); + // @ts-expect-error avoid typecheck error + new Widget({ + siteKey: { + instanceUrl: new URL(props.instanceUrl), + key: props.sitekey, + }, + }); } else { window.setTimeout(requestRender, 1); } @@ -105,14 +121,27 @@ function callback(response?: string) { emit('update:modelValue', typeof response === 'string' ? response : null); } +function onReceivedMessage(message: MessageEvent) { + if (message.data.token) { + if (props.instanceUrl && new URL(message.origin).host === new URL(props.instanceUrl).host) { + callback(<string>message.data.token); + } + } +} + onMounted(() => { if (available.value) { + window.addEventListener('message', onReceivedMessage); requestRender(); } else { watch(available, requestRender); } }); +onUnmounted(() => { + window.removeEventListener('message', onReceivedMessage); +}); + onBeforeUnmount(() => { reset(); }); diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue index 4a58204b5b..393699da6f 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.vue +++ b/packages/frontend/src/components/MkChannelFollowButton.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ @@ -44,12 +44,12 @@ async function onClick() { try { if (isFollowing.value) { - await os.api('channels/unfollow', { + await misskeyApi('channels/unfollow', { channelId: props.channel.id, }); isFollowing.value = false; } else { - await os.api('channels/follow', { + await misskeyApi('channels/follow', { channelId: props.channel.id, }); isFollowing.value = true; diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index adb3c134ae..82605123c5 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, ref, shallowRef, watch, PropType } from 'vue'; import { Chart } from 'chart.js'; import gradient from 'chartjs-plugin-gradient'; -import * as os from '@/os.js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; @@ -277,7 +277,7 @@ const exportData = () => { }; const fetchFederationChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/federation', { limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/federation', { limit: props.limit, span: props.span }); return { series: [{ name: 'Received', @@ -327,7 +327,7 @@ const fetchFederationChart = async (): Promise<typeof chartData> => { }; const fetchApRequestChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/ap-request', { limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/ap-request', { limit: props.limit, span: props.span }); return { series: [{ name: 'In', @@ -349,7 +349,7 @@ const fetchApRequestChart = async (): Promise<typeof chartData> => { }; const fetchNotesChart = async (type: string): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/notes', { limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/notes', { limit: props.limit, span: props.span }); return { series: [{ name: 'All', @@ -396,7 +396,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { }; const fetchNotesTotalChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/notes', { limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/notes', { limit: props.limit, span: props.span }); return { series: [{ name: 'Combined', @@ -415,7 +415,7 @@ const fetchNotesTotalChart = async (): Promise<typeof chartData> => { }; const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/users', { limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/users', { limit: props.limit, span: props.span }); return { series: [{ name: 'Combined', @@ -443,7 +443,7 @@ const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => { }; const fetchActiveUsersChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/active-users', { limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/active-users', { limit: props.limit, span: props.span }); return { series: [{ name: 'Read & Write', @@ -495,7 +495,7 @@ const fetchActiveUsersChart = async (): Promise<typeof chartData> => { }; const fetchDriveChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/drive', { limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/drive', { limit: props.limit, span: props.span }); return { bytes: true, series: [{ @@ -531,7 +531,7 @@ const fetchDriveChart = async (): Promise<typeof chartData> => { }; const fetchDriveFilesChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/drive', { limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/drive', { limit: props.limit, span: props.span }); return { series: [{ name: 'All', @@ -566,7 +566,7 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => { }; const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { series: [{ name: 'In', @@ -588,7 +588,7 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => { }; const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { series: [{ name: 'Users', @@ -603,7 +603,7 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData }; const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { series: [{ name: 'Notes', @@ -618,7 +618,7 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData }; const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { series: [{ name: 'Following', @@ -641,7 +641,7 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> = }; const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { bytes: true, series: [{ @@ -657,7 +657,7 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char }; const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { series: [{ name: 'Drive files', @@ -672,7 +672,7 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char }; const fetchPerUserNotesChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span }); return { series: [...(props.args.withoutAll ? [] : [{ name: 'All', @@ -704,7 +704,7 @@ const fetchPerUserNotesChart = async (): Promise<typeof chartData> => { }; const fetchPerUserPvChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/user/pv', { userId: props.args.user.id, limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/user/pv', { userId: props.args.user.id, limit: props.limit, span: props.span }); return { series: [{ name: 'Unique PV (user)', @@ -731,7 +731,7 @@ const fetchPerUserPvChart = async (): Promise<typeof chartData> => { }; const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); return { series: [{ name: 'Local', @@ -746,7 +746,7 @@ const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => { }; const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); return { series: [{ name: 'Local', @@ -761,7 +761,7 @@ const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => { }; const fetchPerUserDriveChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span }); + const raw = await misskeyApiGet('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span }); return { series: [{ name: 'Inc', diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index 0ec69a69af..789acbd4c1 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.codeEditorScroller"> <textarea ref="inputEl" - v-model="vModel" + v-model="v" :class="[$style.textarea]" :disabled="disabled" :required="required" @@ -58,7 +58,6 @@ const emit = defineEmits<{ }>(); const { modelValue } = toRefs(props); -const vModel = ref<string>(modelValue.value ?? ''); const v = ref<string>(modelValue.value ?? ''); const focused = ref(false); const changed = ref(false); @@ -79,15 +78,14 @@ const onKeydown = (ev: KeyboardEvent) => { if (ev.code === 'Enter') { const pos = inputEl.value?.selectionStart ?? 0; - const posEnd = inputEl.value?.selectionEnd ?? vModel.value.length; + const posEnd = inputEl.value?.selectionEnd ?? v.value.length; if (pos === posEnd) { - const lines = vModel.value.slice(0, pos).split('\n'); + const lines = v.value.slice(0, pos).split('\n'); const currentLine = lines[lines.length - 1]; const currentLineSpaces = currentLine.match(/^\s+/); const posDelta = currentLineSpaces ? currentLineSpaces[0].length : 0; ev.preventDefault(); - vModel.value = vModel.value.slice(0, pos) + '\n' + (currentLineSpaces ? currentLineSpaces[0] : '') + vModel.value.slice(pos); - v.value = vModel.value; + v.value = v.value.slice(0, pos) + '\n' + (currentLineSpaces ? currentLineSpaces[0] : '') + v.value.slice(pos); nextTick(() => { inputEl.value?.setSelectionRange(pos + 1 + posDelta, pos + 1 + posDelta); }); @@ -97,9 +95,8 @@ const onKeydown = (ev: KeyboardEvent) => { if (ev.key === 'Tab') { const pos = inputEl.value?.selectionStart ?? 0; - const posEnd = inputEl.value?.selectionEnd ?? vModel.value.length; - vModel.value = vModel.value.slice(0, pos) + '\t' + vModel.value.slice(posEnd); - v.value = vModel.value; + const posEnd = inputEl.value?.selectionEnd ?? v.value.length; + v.value = v.value.slice(0, pos) + '\t' + v.value.slice(posEnd); nextTick(() => { inputEl.value?.setSelectionRange(pos + 1, pos + 1); }); diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 9969c10258..7d9b0c603f 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -49,9 +49,9 @@ import bytes from '@/filters/bytes.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { useRouter } from '@/router.js'; import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js'; import { deviceKind } from '@/scripts/device-kind.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index dcaaa72cf4..a2c8fd4fa5 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -35,6 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, defineAsyncComponent, ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { claimAchievement } from '@/scripts/achievements.js'; @@ -144,7 +145,7 @@ function onDrop(ev: DragEvent) { if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); emit('removeFile', file.id); - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, folderId: props.folder.id, }); @@ -160,7 +161,7 @@ function onDrop(ev: DragEvent) { if (folder.id === props.folder.id) return; emit('removeFolder', folder.id); - os.api('drive/folders/update', { + misskeyApi('drive/folders/update', { folderId: folder.id, parentId: props.folder.id, }).then(() => { @@ -214,7 +215,7 @@ function rename() { default: props.folder.name, }).then(({ canceled, result: name }) => { if (canceled) return; - os.api('drive/folders/update', { + misskeyApi('drive/folders/update', { folderId: props.folder.id, name: name, }); @@ -222,7 +223,7 @@ function rename() { } function deleteFolder() { - os.api('drive/folders/delete', { + misskeyApi('drive/folders/delete', { folderId: props.folder.id, }).then(() => { if (defaultStore.state.uploadFolder === props.folder.id) { diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index cac3c17c85..e8c22f5d31 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import * as Misskey from 'misskey-js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -112,7 +112,7 @@ function onDrop(ev: DragEvent) { if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); emit('removeFile', file.id); - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, folderId: props.folder ? props.folder.id : null, }); @@ -126,7 +126,7 @@ function onDrop(ev: DragEvent) { // 移動先が自分自身ならreject if (props.folder && folder.id === props.folder.id) return; emit('removeFolder', folder.id); - os.api('drive/folders/update', { + misskeyApi('drive/folders/update', { folderId: folder.id, parentId: props.folder ? props.folder.id : null, }); diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 00bb0e6e2b..2fa5c1a0f8 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -102,6 +102,7 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue'; import XFolder from '@/components/MkDrive.folder.vue'; import XFile from '@/components/MkDrive.file.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; @@ -254,7 +255,7 @@ function onDrop(ev: DragEvent): any { const file = JSON.parse(driveFile); if (files.value.some(f => f.id === file.id)) return; removeFile(file.id); - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, folderId: folder.value ? folder.value.id : null, }); @@ -270,7 +271,7 @@ function onDrop(ev: DragEvent): any { if (folder.value && droppedFolder.id === folder.value.id) return false; if (folders.value.some(f => f.id === droppedFolder.id)) return false; removeFolder(droppedFolder.id); - os.api('drive/folders/update', { + misskeyApi('drive/folders/update', { folderId: droppedFolder.id, parentId: folder.value ? folder.value.id : null, }).then(() => { @@ -307,7 +308,7 @@ function urlUpload() { placeholder: i18n.ts.uploadFromUrlDescription, }).then(({ canceled, result: url }) => { if (canceled || !url) return; - os.api('drive/files/upload-from-url', { + misskeyApi('drive/files/upload-from-url', { url: url, folderId: folder.value ? folder.value.id : undefined, }); @@ -325,7 +326,7 @@ function createFolder() { placeholder: i18n.ts.folderName, }).then(({ canceled, result: name }) => { if (canceled) return; - os.api('drive/folders/create', { + misskeyApi('drive/folders/create', { name: name, parentId: folder.value ? folder.value.id : undefined, }).then(createdFolder => { @@ -341,7 +342,7 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) { default: folderToRename.name, }).then(({ canceled, result: name }) => { if (canceled) return; - os.api('drive/folders/update', { + misskeyApi('drive/folders/update', { folderId: folderToRename.id, name: name, }).then(updatedFolder => { @@ -352,7 +353,7 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) { } function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { - os.api('drive/folders/delete', { + misskeyApi('drive/folders/delete', { folderId: folderToDelete.id, }).then(() => { // 削除時に親フォルダに移動 @@ -436,7 +437,7 @@ function move(target?: Misskey.entities.DriveFolder) { fetching.value = true; - os.api('drive/folders/show', { + misskeyApi('drive/folders/show', { folderId: target, }).then(folderToMove => { folder.value = folderToMove; @@ -535,7 +536,7 @@ async function fetch() { const foldersMax = 30; const filesMax = 30; - const foldersPromise = os.api('drive/folders', { + const foldersPromise = misskeyApi('drive/folders', { folderId: folder.value ? folder.value.id : null, limit: foldersMax + 1, }).then(fetchedFolders => { @@ -546,7 +547,7 @@ async function fetch() { return fetchedFolders; }); - const filesPromise = os.api('drive/files', { + const filesPromise = misskeyApi('drive/files', { folderId: folder.value ? folder.value.id : null, type: props.type, limit: filesMax + 1, @@ -571,7 +572,7 @@ function fetchMoreFolders() { const max = 30; - os.api('drive/folders', { + misskeyApi('drive/folders', { folderId: folder.value ? folder.value.id : null, type: props.type, untilId: folders.value.at(-1)?.id, @@ -594,7 +595,7 @@ function fetchMoreFiles() { const max = 30; // ファイル一覧取得 - os.api('drive/files', { + misskeyApi('drive/files', { folderId: folder.value ? folder.value.id : null, type: props.type, untilId: files.value.at(-1)?.id, diff --git a/packages/frontend/src/components/MkFeaturedPhotos.vue b/packages/frontend/src/components/MkFeaturedPhotos.vue index 6d1bad7433..8a23d7d4bf 100644 --- a/packages/frontend/src/components/MkFeaturedPhotos.vue +++ b/packages/frontend/src/components/MkFeaturedPhotos.vue @@ -10,11 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import * as Misskey from 'misskey-js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const meta = ref<Misskey.entities.MetaResponse>(); -os.api('meta', { detail: true }).then(gotMeta => { +misskeyApi('meta', { detail: true }).then(gotMeta => { meta.value = gotMeta; }); </script> diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index d1b1956a03..1aae460117 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -38,11 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only import { onBeforeUnmount, onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { $i } from '@/account.js'; -import { defaultStore } from "@/store.js"; +import { defaultStore } from '@/store.js'; const props = withDefaults(defineProps<{ user: Misskey.entities.UserDetailed, @@ -63,7 +64,7 @@ const wait = ref(false); const connection = useStream().useChannel('main'); if (props.user.isFollowing == null) { - os.api('users/show', { + misskeyApi('users/show', { userId: props.user.id, }) .then(onFollowChange); @@ -88,17 +89,17 @@ async function onClick() { if (canceled) return; - await os.api('following/delete', { + await misskeyApi('following/delete', { userId: props.user.id, }); } else { if (hasPendingFollowRequestFromYou.value) { - await os.api('following/requests/cancel', { + await misskeyApi('following/requests/cancel', { userId: props.user.id, }); hasPendingFollowRequestFromYou.value = false; } else { - await os.api('following/create', { + await misskeyApi('following/create', { userId: props.user.id, withReplies: defaultStore.state.defaultWithReplies, }); diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index 6f882cfab7..2095a1dcea 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <MkSpacer :marginMin="20" :marginMax="32"> - <div class="_gaps_m"> + <div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m"> <template v-for="item in Object.keys(form).filter(item => !form[item].hidden)"> <MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1"> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template> @@ -55,6 +55,10 @@ SPDX-License-Identifier: AGPL-3.0-only </MkButton> </template> </div> + <div v-else class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.nothing }}</div> + </div> </MkSpacer> </MkModalWindow> </template> @@ -70,6 +74,7 @@ import MkButton from './MkButton.vue'; import MkRadios from './MkRadios.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; +import { infoImageUrl } from '@/instance.js'; const props = defineProps<{ title: string; diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue index a57e6c9292..f47b680f83 100644 --- a/packages/frontend/src/components/MkHeatmap.vue +++ b/packages/frontend/src/components/MkHeatmap.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, nextTick, watch, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { alpha } from '@/scripts/color.js'; @@ -72,19 +72,19 @@ async function renderChart() { let values; if (props.src === 'active-users') { - const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' }); values = raw.readWrite; } else if (props.src === 'notes') { - const raw = await os.api('charts/notes', { limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/notes', { limit: chartLimit, span: 'day' }); values = raw.local.inc; } else if (props.src === 'ap-requests-inbox-received') { - const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' }); values = raw.inboxReceived; } else if (props.src === 'ap-requests-deliver-succeeded') { - const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' }); values = raw.deliverSucceeded; } else if (props.src === 'ap-requests-deliver-failed') { - const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' }); values = raw.deliverFailed; } diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue index 9cde197e19..a188e71174 100644 --- a/packages/frontend/src/components/MkInstanceCardMini.vue +++ b/packages/frontend/src/components/MkInstanceCardMini.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkMiniChart from '@/components/MkMiniChart.vue'; -import * as os from '@/os.js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const props = defineProps<{ @@ -27,7 +27,7 @@ const props = defineProps<{ const chartValues = ref<number[] | null>(null); -os.apiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => { +misskeyApiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => { // 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く res['requests.received'].splice(0, 1); chartValues.value = res['requests.received']; diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue index 7b763ad385..1576089657 100644 --- a/packages/frontend/src/components/MkInstanceStats.vue +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -90,6 +90,7 @@ import MkSelect from '@/components/MkSelect.vue'; import MkChart from '@/components/MkChart.vue'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import * as os from '@/os.js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import MkHeatmap from '@/components/MkHeatmap.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; @@ -162,7 +163,7 @@ function createDoughnut(chartEl, tooltip, data) { } onMounted(() => { - os.apiGet('federation/stats', { limit: 30 }).then(fedStats => { + misskeyApiGet('federation/stats', { limit: 30 }).then(fedStats => { createDoughnut(subDoughnutEl.value, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 870816b839..7870e1e4b8 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -190,6 +190,7 @@ import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import * as os from '@/os.js'; import * as sound from '@/scripts/sound.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore, noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; @@ -207,7 +208,7 @@ import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { shouldCollapsed } from '@/scripts/collapsed.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -319,7 +320,7 @@ const keymap = { }; provide('react', (reaction: string) => { - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, }); @@ -340,7 +341,7 @@ if (props.mock) { if (!props.mock) { useTooltip(renoteButton, async (showing) => { - const renotes = await os.api('notes/renotes', { + const renotes = await misskeyApi('notes/renotes', { noteId: appearNote.value.id, limit: 11, }); @@ -358,7 +359,7 @@ if (!props.mock) { }); useTooltip(quoteButton, async (showing) => { - const renotes = await os.api('notes/renotes', { + const renotes = await misskeyApi('notes/renotes', { noteId: appearNote.value.id, limit: 11, quote: true, @@ -377,7 +378,7 @@ if (!props.mock) { }); if ($i) { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -452,7 +453,7 @@ function renote(visibility: Visibility | 'local') { } if (!props.mock) { - os.api('notes/create', { + misskeyApi('notes/create', { renoteId: appearNote.value.id, channelId: appearNote.value.channelId, }).then(() => { @@ -478,7 +479,7 @@ function renote(visibility: Visibility | 'local') { } if (!props.mock) { - os.api('notes/create', { + misskeyApi('notes/create', { localOnly: visibility === 'local' ? true : localOnlySetting, visibility: noteVisibility, renoteId: appearNote.value.id, @@ -502,7 +503,7 @@ function quote() { renote: appearNote.value, channel: appearNote.value.channel, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -524,7 +525,7 @@ function quote() { os.post({ renote: appearNote.value, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -566,7 +567,7 @@ function like(): void { if (props.mock) { return; } - os.api('notes/like', { + misskeyApi('notes/like', { noteId: appearNote.value.id, override: defaultLike.value, }); @@ -589,7 +590,7 @@ function react(viaKeyboard = false): void { return; } - os.api('notes/like', { + misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, override: defaultLike.value, }); @@ -610,7 +611,7 @@ function react(viaKeyboard = false): void { return; } - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, }); @@ -632,7 +633,7 @@ function undoReact(note): void { return; } - os.api('notes/reactions/delete', { + misskeyApi('notes/reactions/delete', { noteId: note.id, }); } @@ -641,7 +642,7 @@ function undoRenote(note) : void { if (props.mock) { return; } - os.api('notes/unrenote', { + misskeyApi('notes/unrenote', { noteId: note.id, }); os.toast(i18n.ts.rmboost); @@ -718,7 +719,7 @@ function showRenoteMenu(viaKeyboard = false): void { icon: 'ph-trash ph-bold ph-lg', danger: true, action: () => { - os.api('notes/delete', { + misskeyApi('notes/delete', { noteId: note.value.id, }); isDeleted.value = true; @@ -776,7 +777,7 @@ function focusAfter() { } function readPromo() { - os.api('promo/read', { + misskeyApi('promo/read', { noteId: appearNote.value.id, }); isDeleted.value = true; diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 537a99dc01..317666a5e2 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -237,6 +237,7 @@ import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import { notePage } from '@/filters/note.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import * as sound from '@/scripts/sound.js'; import { defaultStore, noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; @@ -327,7 +328,7 @@ watch(() => props.expandAllCws, (expandAllCws) => { }); if ($i) { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -346,7 +347,7 @@ const keymap = { }; provide('react', (reaction: string) => { - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, }); @@ -394,7 +395,7 @@ useNoteCapture({ }); useTooltip(renoteButton, async (showing) => { - const renotes = await os.api('notes/renotes', { + const renotes = await misskeyApi('notes/renotes', { noteId: appearNote.value.id, limit: 11, }); @@ -412,7 +413,7 @@ useTooltip(renoteButton, async (showing) => { }); useTooltip(quoteButton, async (showing) => { - const renotes = await os.api('notes/renotes', { + const renotes = await misskeyApi('notes/renotes', { noteId: appearNote.value.id, limit: 11, quote: true, @@ -493,7 +494,7 @@ function renote(visibility: Visibility | 'local') { os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - os.api('notes/create', { + misskeyApi('notes/create', { renoteId: appearNote.value.id, channelId: appearNote.value.channelId, }).then(() => { @@ -517,7 +518,7 @@ function renote(visibility: Visibility | 'local') { noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home'); } - os.api('notes/create', { + misskeyApi('notes/create', { localOnly: visibility === 'local' ? true : localOnlySetting, visibility: noteVisibility, renoteId: appearNote.value.id, @@ -537,7 +538,7 @@ function quote() { renote: appearNote.value, channel: appearNote.value.channel, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -559,7 +560,7 @@ function quote() { os.post({ renote: appearNote.value, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -596,7 +597,7 @@ function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { - os.api('notes/like', { + misskeyApi('notes/like', { noteId: appearNote.value.id, override: defaultLike.value, }); @@ -612,7 +613,7 @@ function react(viaKeyboard = false): void { reactionPicker.show(reactButton.value, reaction => { sound.play('reaction'); - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, }); @@ -629,7 +630,7 @@ function like(): void { pleaseLogin(); showMovedDialog(); sound.play('reaction'); - os.api('notes/like', { + misskeyApi('notes/like', { noteId: appearNote.value.id, override: defaultLike.value, }); @@ -645,14 +646,14 @@ function like(): void { function undoReact(note): void { const oldReaction = note.myReaction; if (!oldReaction) return; - os.api('notes/reactions/delete', { + misskeyApi('notes/reactions/delete', { noteId: note.id, }); } function undoRenote() : void { if (!renoted.value) return; - os.api('notes/unrenote', { + misskeyApi('notes/unrenote', { noteId: appearNote.value.id, }); os.toast(i18n.ts.rmboost); @@ -712,7 +713,7 @@ function showRenoteMenu(viaKeyboard = false): void { icon: 'ph-trash ph-bold ph-lg', danger: true, action: () => { - os.api('notes/delete', { + misskeyApi('notes/delete', { noteId: note.value.id, }); isDeleted.value = true; @@ -734,7 +735,7 @@ const repliesLoaded = ref(false); function loadReplies() { repliesLoaded.value = true; - os.api('notes/children', { + misskeyApi('notes/children', { noteId: appearNote.value.id, limit: 30, showQuotes: false, @@ -749,7 +750,7 @@ const quotesLoaded = ref(false); function loadQuotes() { quotesLoaded.value = true; - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, limit: 30, quote: true, @@ -764,7 +765,7 @@ const conversationLoaded = ref(false); function loadConversation() { conversationLoaded.value = true; - os.api('notes/conversation', { + misskeyApi('notes/conversation', { noteId: appearNote.value.replyId, }).then(res => { conversation.value = res.reverse(); diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index d725ca15af..9d403bf09e 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -92,6 +92,7 @@ import MkCwButton from '@/components/MkCwButton.vue'; import { notePage } from '@/filters/note.js'; import * as os from '@/os.js'; import * as sound from '@/scripts/sound.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import { userPage } from '@/filters/user.js'; @@ -166,7 +167,7 @@ useNoteCapture({ }); if ($i) { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -196,7 +197,7 @@ function react(viaKeyboard = false): void { showMovedDialog(); sound.play('reaction'); if (props.note.reactionAcceptance === 'likeOnly') { - os.api('notes/like', { + misskeyApi('notes/like', { noteId: props.note.id, override: defaultLike.value, }); @@ -210,7 +211,7 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: props.note.id, reaction: reaction, }); @@ -227,7 +228,7 @@ function like(): void { pleaseLogin(); showMovedDialog(); sound.play('reaction'); - os.api('notes/like', { + misskeyApi('notes/like', { noteId: props.note.id, override: defaultLike.value, }); @@ -243,14 +244,14 @@ function like(): void { function undoReact(note): void { const oldReaction = note.myReaction; if (!oldReaction) return; - os.api('notes/reactions/delete', { + misskeyApi('notes/reactions/delete', { noteId: note.id, }); } function undoRenote() : void { if (!renoted.value) return; - os.api('notes/unrenote', { + misskeyApi('notes/unrenote', { noteId: appearNote.value.id, }); os.toast(i18n.ts.rmboost); @@ -324,7 +325,7 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - os.api('notes/create', { + misskeyApi('notes/create', { renoteId: props.note.id, channelId: props.note.channelId, }).then(() => { @@ -340,7 +341,7 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - os.api('notes/create', { + misskeyApi('notes/create', { renoteId: props.note.id, localOnly: visibility === 'local' ? true : false, visibility: visibility === 'local' || visibility === 'specified' ? props.note.visibility : visibility, @@ -360,7 +361,7 @@ function quote() { renote: appearNote.value, channel: appearNote.value.channel, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: props.note.id, userId: $i.id, limit: 1, @@ -382,7 +383,7 @@ function quote() { os.post({ renote: appearNote.value, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: props.note.id, userId: $i.id, limit: 1, @@ -411,7 +412,7 @@ function menu(viaKeyboard = false): void { } if (props.detail) { - os.api('notes/children', { + misskeyApi('notes/children', { noteId: props.note.id, limit: numberOfReplies.value, showQuotes: false, diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index ed79ca0d86..455a7c9429 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -145,7 +145,7 @@ import { getNoteSummary } from '@/scripts/get-note-summary.js'; import { notePage } from '@/filters/note.js'; import { userPage } from '@/filters/user.js'; import { i18n } from '@/i18n.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; import { infoImageUrl } from '@/instance.js'; @@ -162,12 +162,12 @@ const followRequestDone = ref(false); const acceptFollowRequest = () => { followRequestDone.value = true; - os.api('following/requests/accept', { userId: props.notification.user.id }); + misskeyApi('following/requests/accept', { userId: props.notification.user.id }); }; const rejectFollowRequest = () => { followRequestDone.value = true; - os.api('following/requests/reject', { userId: props.notification.user.id }); + misskeyApi('following/requests/reject', { userId: props.notification.user.id }); }; </script> diff --git a/packages/frontend/src/components/MkNumber.vue b/packages/frontend/src/components/MkNumber.vue index aa04ab253b..1ba4d713b0 100644 --- a/packages/frontend/src/components/MkNumber.vue +++ b/packages/frontend/src/components/MkNumber.vue @@ -9,7 +9,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { reactive, watch } from 'vue'; -import gsap from 'gsap'; import number from '@/filters/number.js'; const props = defineProps<{ @@ -20,8 +19,24 @@ const tweened = reactive({ number: 0, }); -watch(() => props.value, (n) => { - gsap.to(tweened, { duration: 1, number: Number(n) || 0 }); +watch(() => props.value, (to, from) => { + // requestAnimationFrameを利用して、500msでfromからtoまでを1次関数的に変化させる + let start: number | null = null; + + function step(timestamp: number) { + if (start === null) { + start = timestamp; + } + const elapsed = timestamp - start; + tweened.number = (from ?? 0) + (to - (from ?? 0)) * elapsed / 500; + if (elapsed < 500) { + window.requestAnimationFrame(step); + } else { + tweened.number = to; + } + } + + window.requestAnimationFrame(step); }, { immediate: true, }); diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 13a703e9f6..2696baa0d6 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -23,26 +23,26 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div ref="contents" :class="$style.root" style="container-type: inline-size;"> - <RouterView :key="reloadCount" :router="router"/> + <RouterView :key="reloadCount" :router="windowRouter"/> </div> </MkWindow> </template> <script lang="ts" setup> -import { ComputedRef, onMounted, onUnmounted, provide, shallowRef, ref, computed } from 'vue'; +import { computed, ComputedRef, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue'; import RouterView from '@/components/global/RouterView.vue'; import MkWindow from '@/components/MkWindow.vue'; import { popout as _popout } from '@/scripts/popout.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { url } from '@/config.js'; -import { mainRouter, routes, page } from '@/router.js'; -import { $i } from '@/account.js'; -import { Router, useScrollPositionManager } from '@/nirax.js'; +import { useScrollPositionManager } from '@/nirax.js'; import { i18n } from '@/i18n.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { openingWindowsCount } from '@/os.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { getScrollContainer } from '@/scripts/scroll.js'; +import { useRouterFactory } from '@/global/router/supplier.js'; +import { mainRouter } from '@/global/router/main.js'; const props = defineProps<{ initialPath: string; @@ -52,14 +52,15 @@ defineEmits<{ (ev: 'closed'): void; }>(); -const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue'))); +const routerFactory = useRouterFactory(); +const windowRouter = routerFactory(props.initialPath); const contents = shallowRef<HTMLElement>(); const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); const windowEl = shallowRef<InstanceType<typeof MkWindow>>(); const history = ref<{ path: string; key: any; }[]>([{ - path: router.getCurrentPath(), - key: router.getCurrentKey(), + path: windowRouter.getCurrentPath(), + key: windowRouter.getCurrentKey(), }]); const buttonsLeft = computed(() => { const buttons = []; @@ -88,11 +89,11 @@ const buttonsRight = computed(() => { }); const reloadCount = ref(0); -router.addListener('push', ctx => { +windowRouter.addListener('push', ctx => { history.value.push({ path: ctx.path, key: ctx.key }); }); -provide('router', router); +provide('router', windowRouter); provideMetadataReceiver((info) => { pageMetadata.value = info; }); @@ -113,20 +114,20 @@ const contextmenu = computed(() => ([{ icon: 'ph-arrow-square-out ph-bold ph-lg', text: i18n.ts.openInNewTab, action: () => { - window.open(url + router.getCurrentPath(), '_blank', 'noopener'); + window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener'); windowEl.value.close(); }, }, { icon: 'ph-link ph-bold ph-lg', text: i18n.ts.copyLink, action: () => { - copyToClipboard(url + router.getCurrentPath()); + copyToClipboard(url + windowRouter.getCurrentPath()); }, }])); function back() { history.value.pop(); - router.replace(history.value.at(-1)!.path, history.value.at(-1)!.key); + windowRouter.replace(history.value.at(-1)!.path, history.value.at(-1)!.key); } function reload() { @@ -138,16 +139,16 @@ function close() { } function expand() { - mainRouter.push(router.getCurrentPath(), 'forcePage'); + mainRouter.push(windowRouter.getCurrentPath(), 'forcePage'); windowEl.value.close(); } function popout() { - _popout(router.getCurrentPath(), windowEl.value.$el); + _popout(windowRouter.getCurrentPath(), windowEl.value.$el); windowEl.value.close(); } -useScrollPositionManager(() => getScrollContainer(contents.value), router); +useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter); onMounted(() => { openingWindowsCount.value++; diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index bdd96238d3..f5b238046a 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -46,6 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js'; import { useDocumentVisibility } from '@/scripts/use-document-visibility.js'; import { defaultStore } from '@/store.js'; @@ -203,7 +204,7 @@ async function init(): Promise<void> { queue.value = new Map(); fetching.value = true; const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; - await os.api(props.pagination.endpoint, { + await misskeyApi(props.pagination.endpoint, { ...params, limit: props.pagination.limit ?? 10, allowPartial: true, @@ -239,7 +240,7 @@ const fetchMore = async (): Promise<void> => { if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; moreFetching.value = true; const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; - await os.api(props.pagination.endpoint, { + await misskeyApi(props.pagination.endpoint, { ...params, limit: SECOND_FETCH_LIMIT, ...(props.pagination.offsetMode ? { @@ -303,7 +304,7 @@ const fetchMoreAhead = async (): Promise<void> => { if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; moreFetching.value = true; const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; - await os.api(props.pagination.endpoint, { + await misskeyApi(props.pagination.endpoint, { ...params, limit: SECOND_FETCH_LIMIT, ...(props.pagination.offsetMode ? { diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue index 85dd402730..8fbb0debb5 100644 --- a/packages/frontend/src/components/MkPasswordDialog.vue +++ b/packages/frontend/src/components/MkPasswordDialog.vue @@ -41,7 +41,9 @@ import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; + +const $i = signinRequired(); const emit = defineEmits<{ (ev: 'done', v: { password: string; token: string | null; }): void; diff --git a/packages/frontend/src/components/MkPlusOneEffect.vue b/packages/frontend/src/components/MkPlusOneEffect.vue index a741a3f7a8..e5e5a9edf4 100644 --- a/packages/frontend/src/components/MkPlusOneEffect.vue +++ b/packages/frontend/src/components/MkPlusOneEffect.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }"> - <span class="text" :class="{ up }">+1</span> + <span class="text" :class="{ up }">+{{ value }}</span> </div> </template> @@ -16,7 +16,9 @@ import * as os from '@/os.js'; const props = withDefaults(defineProps<{ x: number; y: number; + value?: number; }>(), { + value: 1, }); const emit = defineEmits<{ @@ -40,6 +42,7 @@ onMounted(() => { <style lang="scss" module> .root { + user-select: none; pointer-events: none; position: fixed; width: 128px; diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 6ee0c44658..1322cfdcb6 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -35,11 +35,13 @@ import * as Misskey from 'misskey-js'; import { sum } from '@/scripts/array.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { useInterval } from '@/scripts/use-interval.js'; +import { WithNonNullable } from '@/type.js'; const props = defineProps<{ - note: Misskey.entities.Note; + note: WithNonNullable<Misskey.entities.Note, 'poll'>; readOnly?: boolean; }>(); @@ -94,7 +96,7 @@ const vote = async (id) => { if (canceled) return; } - await os.api('notes/polls/vote', { + await misskeyApi('notes/polls/vote', { noteId: props.note.id, choice: id, }); diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index aa37cef6c2..d76a5e1e04 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -116,12 +116,13 @@ import { extractMentions } from '@/scripts/extract-mentions.js'; import { formatTimeString } from '@/scripts/format-time-string.js'; import { Autocomplete } from '@/scripts/autocomplete.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { selectFiles } from '@/scripts/select-file.js'; import { defaultStore, notePostInterruptors, postFormActions } from '@/store.js'; import MkInfo from '@/components/MkInfo.vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { $i, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account.js'; +import { signinRequired, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account.js'; import { uploadFile } from '@/scripts/upload.js'; import { deepClone } from '@/scripts/clone.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; @@ -130,6 +131,8 @@ import { claimAchievement } from '@/scripts/achievements.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; +const $i = signinRequired(); + const modal = inject('modal'); const props = withDefaults(defineProps<{ @@ -309,7 +312,7 @@ if (props.reply && props.reply.text != null) { } } -if ($i?.isSilenced && visibility.value === 'public') { +if ($i.isSilenced && visibility.value === 'public') { visibility.value = 'home'; } @@ -330,7 +333,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib if (visibility.value === 'specified') { if (props.reply.visibleUserIds) { - os.api('users/show', { + misskeyApi('users/show', { userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId), }).then(users => { users.forEach(pushVisibleUser); @@ -338,7 +341,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib } if (props.reply.userId !== $i.id) { - os.api('users/show', { userId: props.reply.userId }).then(user => { + misskeyApi('users/show', { userId: props.reply.userId }).then(user => { pushVisibleUser(user); }); } @@ -389,7 +392,7 @@ function addMissingMention() { for (const x of extractMentions(ast)) { if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) { - os.api('users/show', { username: x.username, host: x.host }).then(user => { + misskeyApi('users/show', { username: x.username, host: x.host }).then(user => { visibleUsers.value.push(user); }); } @@ -466,7 +469,7 @@ function setVisibility() { os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { currentVisibility: visibility.value, - isSilenced: $i?.isSilenced, + isSilenced: $i.isSilenced, localOnly: localOnly.value, src: visibilityButton.value, }, { @@ -759,7 +762,17 @@ async function post(ev?: MouseEvent) { if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') { const hashtags_ = hashtags.value.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); - postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_; + if (!postData.text) { + postData.text = hashtags_; + } else { + const postTextLines = postData.text.split('\n'); + if (postTextLines[postTextLines.length - 1].trim() === '') { + postTextLines[postTextLines.length - 1] += hashtags_; + } else { + postTextLines[postTextLines.length - 1] += ' ' + hashtags_; + } + postData.text = postTextLines.join('\n'); + } } // plugin @@ -781,7 +794,7 @@ async function post(ev?: MouseEvent) { } posting.value = true; - os.api(postData.editId ? 'notes/edit' : 'notes/create', postData, token).then(() => { + misskeyApi(postData.editId ? 'notes/edit' : 'notes/create', postData, token).then(() => { if (props.freezeAfterPosted) { posted.value = true; } else { diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index b2597d090b..82ed809650 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -24,6 +24,7 @@ import { defineAsyncComponent, inject } from 'vue'; import * as Misskey from 'misskey-js'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -61,7 +62,7 @@ function toggleSensitive(file) { return; } - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, isSensitive: !file.isSensitive, }).then(() => { @@ -78,7 +79,7 @@ async function rename(file) { allowEmpty: false, }); if (canceled) return; - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, name: result, }).then(() => { @@ -96,7 +97,7 @@ async function describe(file) { }, { done: caption => { let comment = caption.length === 0 ? null : caption; - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, comment: comment, }).then(() => { diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue index ebbd5e6cdc..1b8263ae67 100644 --- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue +++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue @@ -45,7 +45,8 @@ import { ref } from 'vue'; import { $i, getAccounts } from '@/account.js'; import MkButton from '@/components/MkButton.vue'; import { instance } from '@/instance.js'; -import { api, apiWithDialog, promiseDialog } from '@/os.js'; +import { apiWithDialog, promiseDialog } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; defineProps<{ @@ -82,7 +83,7 @@ function subscribe() { pushSubscription.value = subscription; // Register - pushRegistrationInServer.value = await api('sw/register', { + pushRegistrationInServer.value = await misskeyApi('sw/register', { endpoint: subscription.endpoint, auth: encode(subscription.getKey('auth')), publickey: encode(subscription.getKey('p256dh')), @@ -159,7 +160,7 @@ if (navigator.serviceWorker == null) { supported.value = true; if (pushSubscription.value) { - const res = await api('sw/show-registration', { + const res = await misskeyApi('sw/show-registration', { endpoint: pushSubscription.value.endpoint, }); diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index c6ef05acef..a3791aee07 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -22,6 +22,7 @@ import * as Misskey from 'misskey-js'; import XDetails from '@/components/MkReactionsViewer.details.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import * as os from '@/os.js'; +import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import { $i } from '@/account.js'; import MkReactionEffect from '@/components/MkReactionEffect.vue'; @@ -69,11 +70,11 @@ async function toggleReaction() { return; } - os.api('notes/reactions/delete', { + misskeyApi('notes/reactions/delete', { noteId: props.note.id, }).then(() => { if (oldReaction !== props.reaction) { - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: props.note.id, reaction: props.reaction, }); @@ -87,7 +88,7 @@ async function toggleReaction() { return; } - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: props.note.id, reaction: props.reaction, }); @@ -117,7 +118,7 @@ onMounted(() => { if (!mock) { useTooltip(buttonEl, async (showing) => { - const reactions = await os.apiGet('notes/reactions', { + const reactions = await misskeyApiGet('notes/reactions', { noteId: props.note.id, type: props.reaction, limit: 10, diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue index e69aa1be80..ef497e0e82 100644 --- a/packages/frontend/src/components/MkRetentionHeatmap.vue +++ b/packages/frontend/src/components/MkRetentionHeatmap.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, nextTick, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { alpha } from '@/scripts/color.js'; @@ -43,7 +43,7 @@ async function renderChart() { const maxDays = wide ? 10 : narrow ? 5 : 7; - let raw = await os.api('retention', { }); + let raw = await misskeyApi('retention', { }); raw = raw.slice(0, maxDays + 1); diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue index e2682ec06b..eb05878ae8 100644 --- a/packages/frontend/src/components/MkRetentionLineChart.vue +++ b/packages/frontend/src/components/MkRetentionLineChart.vue @@ -16,7 +16,7 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; import { alpha } from '@/scripts/color.js'; import { initChart } from '@/scripts/init-chart.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; initChart(); @@ -40,7 +40,7 @@ const getDate = (ymd: string) => { }; onMounted(async () => { - let raw = await os.api('retention', { }); + let raw = await misskeyApi('retention', { }); const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; diff --git a/packages/frontend/src/components/MkRippleEffect.vue b/packages/frontend/src/components/MkRippleEffect.vue index 860b083327..11f1aec30f 100644 --- a/packages/frontend/src/components/MkRippleEffect.vue +++ b/packages/frontend/src/components/MkRippleEffect.vue @@ -77,7 +77,14 @@ const emit = defineEmits<{ (ev: 'end'): void; }>(); -const particles = []; +const particles: { + size: number; + xA: number; + yA: number; + xB: number; + yB: number; + color: string; +}[] = []; const origin = 64; const colors = ['#FF1493', '#00FFFF', '#FFE202']; const zIndex = os.claimZIndex('high'); diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index c884ce53ea..278e2d6054 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -59,6 +59,7 @@ import MkInput from '@/components/MkInput.vue'; import MkInfo from '@/components/MkInfo.vue'; import { host as configHost } from '@/config.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; @@ -95,7 +96,7 @@ const props = defineProps({ }); function onUsernameChange(): void { - os.api('users/show', { + misskeyApi('users/show', { username: username.value, }).then(userResponse => { user.value = userResponse; @@ -120,7 +121,7 @@ async function queryKey(): Promise<void> { credentialRequest.value = null; queryingKey.value = false; signing.value = true; - return os.api('signin', { + return misskeyApi('signin', { username: username.value, password: password.value, credential: credential.toJSON(), @@ -142,7 +143,7 @@ function onSubmit(): void { signing.value = true; if (!totpLogin.value && user.value && user.value.twoFactorEnabled) { if (webAuthnSupported() && user.value.securityKeys) { - os.api('signin', { + misskeyApi('signin', { username: username.value, password: password.value, }).then(res => { @@ -159,7 +160,7 @@ function onSubmit(): void { signing.value = false; } } else { - os.api('signin', { + misskeyApi('signin', { username: username.value, password: password.value, token: user.value?.twoFactorEnabled ? token.value : undefined, diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 9984b09c1a..98eeaed066 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -67,6 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix><i class="ph-chalkboard-teacher ph-bold ph-lg"></i></template> </MkInput> <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/> + <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/> <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> <MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;"> @@ -88,6 +89,7 @@ import MkInput from './MkInput.vue'; import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; import * as config from '@/config.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; @@ -122,6 +124,7 @@ const passwordStrength = ref<'' | 'low' | 'medium' | 'high'>(''); const passwordRetypeState = ref<null | 'match' | 'not-match'>(null); const submitting = ref<boolean>(false); const hCaptchaResponse = ref<string | null>(null); +const mCaptchaResponse = ref<string | null>(null); const reCaptchaResponse = ref<string | null>(null); const turnstileResponse = ref<string | null>(null); const usernameAbortController = ref<null | AbortController>(null); @@ -130,6 +133,7 @@ const emailAbortController = ref<null | AbortController>(null); const shouldDisableSubmitting = computed((): boolean => { return submitting.value || instance.enableHcaptcha && !hCaptchaResponse.value || + instance.enableMcaptcha && !mCaptchaResponse.value || instance.enableRecaptcha && !reCaptchaResponse.value || instance.enableTurnstile && !turnstileResponse.value || instance.emailRequiredForSignup && emailState.value !== 'ok' || @@ -186,7 +190,7 @@ function onChangeUsername(): void { usernameState.value = 'wait'; usernameAbortController.value = new AbortController(); - os.api('username/available', { + misskeyApi('username/available', { username: username.value, }, undefined, usernameAbortController.value.signal).then(result => { usernameState.value = result.available ? 'ok' : 'unavailable'; @@ -209,7 +213,7 @@ function onChangeEmail(): void { emailState.value = 'wait'; emailAbortController.value = new AbortController(); - os.api('email-address/available', { + misskeyApi('email-address/available', { emailAddress: email.value, }, undefined, emailAbortController.value.signal).then(result => { emailState.value = result.available ? 'ok' : @@ -251,13 +255,14 @@ async function onSubmit(): Promise<void> { submitting.value = true; try { - await os.api('signup', { + await misskeyApi('signup', { username: username.value, password: password.value, emailAddress: email.value, invitationCode: invitationCode.value, reason: reason.value, 'hcaptcha-response': hCaptchaResponse.value, + 'm-captcha-response': mCaptchaResponse.value, 'g-recaptcha-response': reCaptchaResponse.value, }); if (instance.emailRequiredForSignup) { @@ -275,7 +280,7 @@ async function onSubmit(): Promise<void> { }); emit('approvalPending'); } else { - const res = await os.api('signin', { + const res = await misskeyApi('signin', { username: username.value, password: password.value, }); diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index c071fb938a..1ad213912f 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -48,7 +48,7 @@ import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { shouldCollapsed } from '@/scripts/collapsed.js'; import { defaultStore } from '@/store.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; import * as os from '@/os.js'; import { checkAnimationFromMfm } from '@/scripts/check-animated-mfm.js'; diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 8bd68c0fd2..00be5d2042 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -11,13 +11,13 @@ SPDX-License-Identifier: AGPL-3.0-only :pagination="paginationQuery" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" @queue="emit('queue', $event)" - @status="prComponent.setDisabled($event)" + @status="prComponent?.setDisabled($event)" /> </MkPullToRefresh> </template> <script lang="ts" setup> -import { computed, watch, onUnmounted, provide, ref } from 'vue'; +import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue'; import { Connection } from 'misskey-js/built/streaming.js'; import MkNotes from '@/components/MkNotes.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; @@ -65,12 +65,14 @@ type TimelineQueryType = { roleId?: string } -const prComponent = ref<InstanceType<typeof MkPullToRefresh>>(); -const tlComponent = ref<InstanceType<typeof MkNotes>>(); +const prComponent = shallowRef<InstanceType<typeof MkPullToRefresh>>(); +const tlComponent = shallowRef<InstanceType<typeof MkNotes>>(); let tlNotesCount = 0; -const prepend = note => { +function prepend(note) { + if (tlComponent.value == null) return; + tlNotesCount++; if (instance.notesPerOneAd > 0 && tlNotesCount % instance.notesPerOneAd === 0) { @@ -84,7 +86,7 @@ const prepend = note => { if (props.sound) { sound.play($i && (note.userId === $i.id) ? 'noteMy' : 'note'); } -}; +} let connection: Connection; let connection2: Connection; @@ -142,6 +144,7 @@ function connectChannel() { connection.on('mention', onNote); } else if (props.src === 'list') { connection = stream.useChannel('userList', { + withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, listId: props.list, }); @@ -219,6 +222,7 @@ function updatePaginationQuery() { } else if (props.src === 'list') { endpoint = 'notes/user-list-timeline'; query = { + withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, listId: props.list, }; @@ -257,8 +261,9 @@ function refreshEndpointAndChannel() { updatePaginationQuery(); } +// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる // IDが切り替わったら切り替え先のTLを表示させたい -watch(() => [props.list, props.antenna, props.channel, props.role], refreshEndpointAndChannel); +watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel); // 初回表示用 refreshEndpointAndChannel(); @@ -269,6 +274,8 @@ onUnmounted(() => { function reloadTimeline() { return new Promise<void>((res) => { + if (tlComponent.value == null) return; + tlNotesCount = 0; tlComponent.value.pagingComponent?.reload().then(() => { diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue index 8e8e26ed5f..d024e1e593 100644 --- a/packages/frontend/src/components/MkTokenGenerateWindow.vue +++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only :withOkButton="true" :okButtonDisabled="false" :canClose="false" - @close="dialog.close()" + @close="dialog?.close()" @closed="$emit('closed')" @ok="ok()" > @@ -87,7 +87,7 @@ function ok(): void { name: name.value, permissions: Object.keys(permissions.value).filter(p => permissions.value[p]), }); - dialog.value.close(); + dialog.value?.close(); } function disableAll(): void { diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue index eeb9325a29..27e5bb1b84 100644 --- a/packages/frontend/src/components/MkTooltip.vue +++ b/packages/frontend/src/components/MkTooltip.vue @@ -13,8 +13,10 @@ SPDX-License-Identifier: AGPL-3.0-only > <div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }"> <slot> - <Mfm v-if="asMfm" :text="text"/> - <span v-else>{{ text }}</span> + <template v-if="text"> + <Mfm v-if="asMfm" :text="text"/> + <span v-else>{{ text }}</span> + </template> </slot> </div> </Transition> @@ -53,6 +55,7 @@ const el = shallowRef<HTMLElement>(); const zIndex = os.claimZIndex('high'); function setPosition() { + if (!el.value || !props.targetElement) return; const data = calcPopupPosition(el.value, { anchorElement: props.targetElement, direction: props.direction, diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue index 07efaf8982..b5f657e951 100644 --- a/packages/frontend/src/components/MkUpdated.vue +++ b/packages/frontend/src/components/MkUpdated.vue @@ -4,12 +4,12 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkModal ref="modal" :zPriority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')"> +<MkModal ref="modal" :zPriority="'middle'" @click="modal?.close()" @closed="$emit('closed')"> <div :class="$style.root"> <div :class="$style.title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div> <div :class="$style.version">✨{{ version }}🚀</div> <MkButton full @click="whatIsNew">{{ i18n.ts.whatIsNew }}</MkButton> - <MkButton :class="$style.gotIt" primary full @click="$refs.modal.close()">{{ i18n.ts.gotIt }}</MkButton> + <MkButton :class="$style.gotIt" primary full @click="modal?.close()">{{ i18n.ts.gotIt }}</MkButton> </div> </MkModal> </template> @@ -26,7 +26,7 @@ import { confetti } from '@/scripts/confetti.js'; const modal = shallowRef<InstanceType<typeof MkModal>>(); const whatIsNew = () => { - modal.value.close(); + modal.value?.close(); window.open(`https://git.joinsharkey.org/Sharkey/Sharkey/releases/tag/${version}`, '_blank'); }; diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 486aaa0bbd..81d0acb8fa 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, onUnmounted, ref } from 'vue'; -import type { summaly } from 'summaly'; +import type { summaly } from '@misskey-dev/summaly'; import { url as local } from '@/config.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index 3fbadbe34f..17ede3e620 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -56,6 +56,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import MkTextarea from '@/components/MkTextarea.vue'; import MkSwitch from '@/components/MkSwitch.vue'; @@ -121,7 +122,7 @@ async function del() { }); if (canceled) return; - os.api('admin/announcements/delete', { + misskeyApi('admin/announcements/delete', { id: props.announcement.id, }).then(() => { emit('done', { diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue index b9c7377972..6068d5d13b 100644 --- a/packages/frontend/src/components/MkUserCardMini.vue +++ b/packages/frontend/src/components/MkUserCardMini.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only import * as Misskey from 'misskey-js'; import { onMounted, ref } from 'vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; -import * as os from '@/os.js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { acct } from '@/filters/user.js'; const props = withDefaults(defineProps<{ @@ -32,7 +32,7 @@ const chartValues = ref<number[] | null>(null); onMounted(() => { if (props.withChart) { - os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16 + 1, span: 'day' }).then(res => { + misskeyApiGet('charts/user/notes', { userId: props.user.id, limit: 16 + 1, span: 'day' }).then(res => { // 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く res.inc.splice(0, 1); chartValues.value = res.inc; diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index c9100652f3..0795b43bfd 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -72,6 +72,7 @@ import * as Misskey from 'misskey-js'; import MkFollowButton from '@/components/MkFollowButton.vue'; import { userPage } from '@/filters/user.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import number from '@/filters/number.js'; import { i18n } from '@/i18n.js'; @@ -109,7 +110,7 @@ onMounted(() => { Misskey.acct.parse(props.q.substring(1)) : { userId: props.q }; - os.api('users/show', query).then(res => { + misskeyApi('users/show', query).then(res => { if (!props.showing) return; user.value = res; }); diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index 9d41147bd2..f4aa06950d 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -62,7 +62,7 @@ import * as Misskey from 'misskey-js'; import MkInput from '@/components/MkInput.vue'; import FormSplit from '@/components/form/split.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; @@ -90,7 +90,7 @@ const search = () => { users.value = []; return; } - os.api('users/search-by-username-and-host', { + misskeyApi('users/search-by-username-and-host', { username: username.value, host: host.value, limit: 10, @@ -118,7 +118,7 @@ const cancel = () => { }; onMounted(() => { - os.api('users/show', { + misskeyApi('users/show', { userIds: defaultStore.state.recentlyUsedUsers, }).then(users => { if (props.includeSelf && users.find(x => $i ? x.id === $i.id : true) == null) { diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue index 664c4da203..ef9f74b95b 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue @@ -41,14 +41,14 @@ import { i18n } from '@/i18n.js'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkFolder from '@/components/MkFolder.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const isLocked = ref(false); const hideOnlineStatus = ref(false); const noCrawle = ref(false); watch([isLocked, hideOnlineStatus, noCrawle], () => { - os.api('i/update', { + misskeyApi('i/update', { isLocked: !!isLocked.value, hideOnlineStatus: !!hideOnlineStatus.value, noCrawle: !!noCrawle.value, diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue index 7becc5c66d..695c0c7843 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.User.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue @@ -29,7 +29,7 @@ import * as Misskey from 'misskey-js'; import { ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const props = defineProps<{ user: Misskey.entities.UserDetailed; @@ -39,7 +39,7 @@ const isFollowing = ref(false); async function follow() { isFollowing.value = true; - os.api('following/create', { + misskeyApi('following/create', { userId: props.user.id, }); } diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue index 746ed3e0de..e45d594f12 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue @@ -17,7 +17,7 @@ import { onMounted, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import gradient from 'chartjs-plugin-gradient'; import tinycolor from 'tinycolor2'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; @@ -53,7 +53,7 @@ async function renderChart() { })); }; - const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' }); const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index 862a38bd54..9f4fc00938 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -63,6 +63,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; import { instanceName } from '@/config.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import MkNumber from '@/components/MkNumber.vue'; @@ -71,11 +72,11 @@ import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart. const meta = ref<Misskey.entities.MetaResponse | null>(null); const stats = ref<Misskey.entities.StatsResponse | null>(null); -os.api('meta', { detail: true }).then(_meta => { +misskeyApi('meta', { detail: true }).then(_meta => { meta.value = _meta; }); -os.api('stats', {}).then((res) => { +misskeyApi('stats', {}).then((res) => { stats.value = res; }); diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index e5b8bd9b15..e897a0b7a6 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -143,6 +143,7 @@ function top() { } function maximize() { + if (rootEl.value == null) return; maximized.value = true; unResizedTop = rootEl.value.style.top; unResizedLeft = rootEl.value.style.left; @@ -155,6 +156,7 @@ function maximize() { } function unMaximize() { + if (rootEl.value == null) return; maximized.value = false; rootEl.value.style.top = unResizedTop; rootEl.value.style.left = unResizedLeft; @@ -163,6 +165,7 @@ function unMaximize() { } function minimize() { + if (rootEl.value == null) return; minimized.value = true; unResizedWidth = rootEl.value.style.width; unResizedHeight = rootEl.value.style.height; @@ -171,8 +174,8 @@ function minimize() { } function unMinimize() { + if (rootEl.value == null) return; const main = rootEl.value; - if (main == null) return; minimized.value = false; rootEl.value.style.width = unResizedWidth; diff --git a/packages/frontend/src/components/SkApprovalUser.vue b/packages/frontend/src/components/SkApprovalUser.vue index 2bf6361ac8..f85944cd04 100644 --- a/packages/frontend/src/components/SkApprovalUser.vue +++ b/packages/frontend/src/components/SkApprovalUser.vue @@ -33,6 +33,7 @@ import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const props = defineProps<{ user: Misskey.entities.User; @@ -42,7 +43,7 @@ let reason = ref(''); let email = ref(''); function getReason() { - return os.api('admin/show-user', { + return misskeyApi('admin/show-user', { userId: props.user.id, }).then(info => { reason.value = info?.signupReason; @@ -87,7 +88,7 @@ async function approveAccount() { text: i18n.ts.approveConfirm, }); if (confirm.canceled) return; - await os.api('admin/approve-user', { userId: props.user.id }); + await misskeyApi('admin/approve-user', { userId: props.user.id }); emits('deleted', props.user.id); } </script> diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index 1603b6acb2..66ef22633d 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -190,6 +190,7 @@ import { focusPrev, focusNext } from '@/scripts/focus.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import * as sound from '@/scripts/sound.js'; import { defaultStore, noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; @@ -208,7 +209,7 @@ import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { shouldCollapsed } from '@/scripts/collapsed.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -320,7 +321,7 @@ const keymap = { }; provide('react', (reaction: string) => { - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, }); @@ -341,7 +342,7 @@ if (props.mock) { if (!props.mock) { useTooltip(renoteButton, async (showing) => { - const renotes = await os.api('notes/renotes', { + const renotes = await misskeyApi('notes/renotes', { noteId: appearNote.value.id, limit: 11, }); @@ -359,7 +360,7 @@ if (!props.mock) { }); useTooltip(quoteButton, async (showing) => { - const renotes = await os.api('notes/renotes', { + const renotes = await misskeyApi('notes/renotes', { noteId: appearNote.value.id, limit: 11, quote: true, @@ -378,7 +379,7 @@ if (!props.mock) { }); if ($i) { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -453,7 +454,7 @@ function renote(visibility: Visibility | 'local') { } if (!props.mock) { - os.api('notes/create', { + misskeyApi('notes/create', { renoteId: appearNote.value.id, channelId: appearNote.value.channelId, }).then(() => { @@ -479,7 +480,7 @@ function renote(visibility: Visibility | 'local') { } if (!props.mock) { - os.api('notes/create', { + misskeyApi('notes/create', { localOnly: visibility === 'local' ? true : localOnlySetting, visibility: noteVisibility, renoteId: appearNote.value.id, @@ -503,7 +504,7 @@ function quote() { renote: appearNote.value, channel: appearNote.value.channel, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -525,7 +526,7 @@ function quote() { os.post({ renote: appearNote.value, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -567,7 +568,7 @@ function like(): void { if (props.mock) { return; } - os.api('notes/like', { + misskeyApi('notes/like', { noteId: appearNote.value.id, override: defaultLike.value, }); @@ -590,7 +591,7 @@ function react(viaKeyboard = false): void { return; } - os.api('notes/like', { + misskeyApi('notes/like', { noteId: appearNote.value.id, override: defaultLike.value, }); @@ -611,7 +612,7 @@ function react(viaKeyboard = false): void { return; } - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, }); @@ -633,7 +634,7 @@ function undoReact(note): void { return; } - os.api('notes/reactions/delete', { + misskeyApi('notes/reactions/delete', { noteId: note.id, }); } @@ -642,7 +643,7 @@ function undoRenote(note) : void { if (props.mock) { return; } - os.api('notes/unrenote', { + misskeyApi('notes/unrenote', { noteId: note.id, }); os.toast(i18n.ts.rmboost); @@ -719,7 +720,7 @@ function showRenoteMenu(viaKeyboard = false): void { icon: 'ph-trash ph-bold ph-lg', danger: true, action: () => { - os.api('notes/delete', { + misskeyApi('notes/delete', { noteId: note.value.id, }); isDeleted.value = true; @@ -781,7 +782,7 @@ function scrollIntoView() { } function readPromo() { - os.api('promo/read', { + misskeyApi('promo/read', { noteId: appearNote.value.id, }); isDeleted.value = true; diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index 4a06e8f56a..212fa99bef 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -245,6 +245,7 @@ import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import { notePage } from '@/filters/note.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import * as sound from '@/scripts/sound.js'; import { defaultStore, noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; @@ -336,7 +337,7 @@ watch(() => props.expandAllCws, (expandAllCws) => { }); if ($i) { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -355,7 +356,7 @@ const keymap = { }; provide('react', (reaction: string) => { - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, }); @@ -403,7 +404,7 @@ useNoteCapture({ }); useTooltip(renoteButton, async (showing) => { - const renotes = await os.api('notes/renotes', { + const renotes = await misskeyApi('notes/renotes', { noteId: appearNote.value.id, limit: 11, }); @@ -421,7 +422,7 @@ useTooltip(renoteButton, async (showing) => { }); useTooltip(quoteButton, async (showing) => { - const renotes = await os.api('notes/renotes', { + const renotes = await misskeyApi('notes/renotes', { noteId: appearNote.value.id, limit: 11, quote: true, @@ -502,7 +503,7 @@ function renote(visibility: Visibility | 'local') { os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - os.api('notes/create', { + misskeyApi('notes/create', { renoteId: appearNote.value.id, channelId: appearNote.value.channelId, }).then(() => { @@ -526,7 +527,7 @@ function renote(visibility: Visibility | 'local') { noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home'); } - os.api('notes/create', { + misskeyApi('notes/create', { localOnly: visibility === 'local' ? true : localOnlySetting, visibility: noteVisibility, renoteId: appearNote.value.id, @@ -546,7 +547,7 @@ function quote() { renote: appearNote.value, channel: appearNote.value.channel, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -568,7 +569,7 @@ function quote() { os.post({ renote: appearNote.value, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -605,7 +606,7 @@ function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { - os.api('notes/like', { + misskeyApi('notes/like', { noteId: appearNote.value.id, override: defaultLike.value, }); @@ -621,7 +622,7 @@ function react(viaKeyboard = false): void { reactionPicker.show(reactButton.value, reaction => { sound.play('reaction'); - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, }); @@ -638,7 +639,7 @@ function like(): void { pleaseLogin(); showMovedDialog(); sound.play('reaction'); - os.api('notes/like', { + misskeyApi('notes/like', { noteId: appearNote.value.id, override: defaultLike.value, }); @@ -654,14 +655,14 @@ function like(): void { function undoReact(note): void { const oldReaction = note.myReaction; if (!oldReaction) return; - os.api('notes/reactions/delete', { + misskeyApi('notes/reactions/delete', { noteId: note.id, }); } function undoRenote() : void { if (!renoted.value) return; - os.api('notes/unrenote', { + misskeyApi('notes/unrenote', { noteId: appearNote.value.id, }); os.toast(i18n.ts.rmboost); @@ -721,7 +722,7 @@ function showRenoteMenu(viaKeyboard = false): void { icon: 'ph-trash ph-bold ph-lg', danger: true, action: () => { - os.api('notes/delete', { + misskeyApi('notes/delete', { noteId: note.value.id, }); isDeleted.value = true; @@ -743,7 +744,7 @@ const repliesLoaded = ref(false); function loadReplies() { repliesLoaded.value = true; - os.api('notes/children', { + misskeyApi('notes/children', { noteId: appearNote.value.id, limit: 30, showQuotes: false, @@ -758,7 +759,7 @@ const quotesLoaded = ref(false); function loadQuotes() { quotesLoaded.value = true; - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, limit: 30, quote: true, @@ -773,7 +774,7 @@ const conversationLoaded = ref(false); function loadConversation() { conversationLoaded.value = true; - os.api('notes/conversation', { + misskeyApi('notes/conversation', { noteId: appearNote.value.replyId, }).then(res => { conversation.value = res.reverse(); diff --git a/packages/frontend/src/components/SkNoteHeader.vue b/packages/frontend/src/components/SkNoteHeader.vue index 7a3f9d02f5..d471c2e6df 100644 --- a/packages/frontend/src/components/SkNoteHeader.vue +++ b/packages/frontend/src/components/SkNoteHeader.vue @@ -82,7 +82,7 @@ import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; import SkInstanceTicker from '@/components/SkInstanceTicker.vue'; import { popupMenu } from '@/os.js'; import { defaultStore } from '@/store.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; import { deviceKind } from '@/scripts/device-kind.js'; const props = defineProps<{ diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue index 8b3ced3761..60a574731f 100644 --- a/packages/frontend/src/components/SkNoteSub.vue +++ b/packages/frontend/src/components/SkNoteSub.vue @@ -99,6 +99,7 @@ import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkCwButton from '@/components/MkCwButton.vue'; import { notePage } from '@/filters/note.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import * as sound from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; @@ -175,7 +176,7 @@ useNoteCapture({ }); if ($i) { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -205,7 +206,7 @@ function react(viaKeyboard = false): void { showMovedDialog(); sound.play('reaction'); if (props.note.reactionAcceptance === 'likeOnly') { - os.api('notes/like', { + misskeyApi('notes/like', { noteId: props.note.id, override: defaultLike.value, }); @@ -219,7 +220,7 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: props.note.id, reaction: reaction, }); @@ -236,7 +237,7 @@ function like(): void { pleaseLogin(); showMovedDialog(); sound.play('reaction'); - os.api('notes/like', { + misskeyApi('notes/like', { noteId: props.note.id, override: defaultLike.value, }); @@ -252,14 +253,14 @@ function like(): void { function undoReact(note): void { const oldReaction = note.myReaction; if (!oldReaction) return; - os.api('notes/reactions/delete', { + misskeyApi('notes/reactions/delete', { noteId: note.id, }); } function undoRenote() : void { if (!renoted.value) return; - os.api('notes/unrenote', { + misskeyApi('notes/unrenote', { noteId: appearNote.value.id, }); os.toast(i18n.ts.rmboost); @@ -333,7 +334,7 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - os.api('notes/create', { + misskeyApi('notes/create', { renoteId: props.note.id, channelId: props.note.channelId, }).then(() => { @@ -349,7 +350,7 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - os.api('notes/create', { + misskeyApi('notes/create', { renoteId: props.note.id, localOnly: visibility === 'local' ? true : false, visibility: visibility === 'local' || visibility === 'specified' ? props.note.visibility : visibility, @@ -369,7 +370,7 @@ function quote() { renote: appearNote.value, channel: appearNote.value.channel, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: props.note.id, userId: $i.id, limit: 1, @@ -391,7 +392,7 @@ function quote() { os.post({ renote: appearNote.value, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: props.note.id, userId: $i.id, limit: 1, @@ -420,7 +421,7 @@ function menu(viaKeyboard = false): void { } if (props.detail) { - os.api('notes/children', { + misskeyApi('notes/children', { noteId: props.note.id, limit: numberOfReplies.value, showQuotes: false, diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index e2b59869a4..2119b33d07 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -15,7 +15,7 @@ import * as os from '@/os.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { url } from '@/config.js'; import { i18n } from '@/i18n.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const props = withDefaults(defineProps<{ to: string; diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index f03bf26fad..8945f2b64b 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -5,15 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick" v-on:click.stop/> -<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle" @click="onClick" v-on:click.stop>{{ props.emoji }}</span> -<span v-else>{{ emoji }}</span> +<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick" v-on:click.stop>{{ colorizedNativeEmoji }}</span> </template> <script lang="ts" setup> import { computed, inject } from 'vue'; import { char2twemojiFilePath, char2fluentEmojiFilePath, char2tossfaceFilePath } from '@/scripts/emoji-base.js'; import { defaultStore } from '@/store.js'; -import { getEmojiName } from '@/scripts/emojilist.js'; +import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js'; import * as os from '@/os.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import * as sound from '@/scripts/sound.js'; @@ -30,9 +29,8 @@ const react = inject<((name: string) => void) | null>('react', null); const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : defaultStore.reactiveState.emojiStyle.value === 'tossface' ? char2tossfaceFilePath : char2fluentEmojiFilePath; const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native'); -const url = computed(() => { - return char2path(props.emoji); -}); +const url = computed(() => char2path(props.emoji)); +const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji)); // Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter function computeTitle(event: PointerEvent): void { diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index a3bfdf0bb4..25118a9cbc 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -48,6 +48,10 @@ type MfmEvents = { clickEv(id: string): void; }; +type MfmEvents = { + clickEv(id: string): void; +}; + // eslint-disable-next-line import/no-default-export export default function(props: MfmProps, context: SetupContext<MfmEvents>) { const isNote = props.isNote ?? true; diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index 99ed8adbef..dc7474835d 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, onBeforeUnmount, provide, shallowRef, ref } from 'vue'; -import { Resolved, Router } from '@/nirax.js'; +import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue'; +import { IRouter, Resolved } from '@/nirax.js'; import { defaultStore } from '@/store.js'; const props = defineProps<{ - router?: Router; + router?: IRouter; }>(); const router = props.router ?? inject('router'); diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue index d885ebb1d6..83fdf24deb 100644 --- a/packages/frontend/src/components/page/page.note.vue +++ b/packages/frontend/src/components/page/page.note.vue @@ -16,7 +16,7 @@ import * as Misskey from 'misskey-js'; import { NoteBlock } from './block.type.js'; import MkNote from '@/components/MkNote.vue'; import MkNoteDetailed from '@/components/MkNoteDetailed.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const props = defineProps<{ block: NoteBlock, @@ -26,7 +26,7 @@ const props = defineProps<{ const note = ref<Misskey.entities.Note | null>(null); onMounted(() => { - os.api('notes/show', { noteId: props.block.note }) + misskeyApi('notes/show', { noteId: props.block.note }) .then(result => { note.value = result; }); diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts index 6a48159f13..e4ce9cb9cd 100644 --- a/packages/frontend/src/custom-emojis.ts +++ b/packages/frontend/src/custom-emojis.ts @@ -5,7 +5,7 @@ import { shallowRef, computed, markRaw, watch } from 'vue'; import * as Misskey from 'misskey-js'; -import { api, apiGet } from '@/os.js'; +import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { get, set } from '@/scripts/idb-proxy.js'; @@ -52,11 +52,11 @@ export async function fetchCustomEmojis(force = false) { let res; if (force) { - res = await api('emojis', {}); + res = await misskeyApi('emojis', {}); } else { const lastFetchedAt = await get('lastEmojisFetchedAt'); if (lastFetchedAt && (now - lastFetchedAt) < 1000 * 60 * 60) return; - res = await apiGet('emojis', {}); + res = await misskeyApiGet('emojis', {}); } customEmojis.value = res.emojis; diff --git a/packages/frontend/src/filters/bytes.ts b/packages/frontend/src/filters/bytes.ts index d40b020a9e..2497ddb775 100644 --- a/packages/frontend/src/filters/bytes.ts +++ b/packages/frontend/src/filters/bytes.ts @@ -5,10 +5,10 @@ export default (v, digits = 0) => { if (v == null) return '?'; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'RB', 'QB']; if (v === 0) return '0'; const isMinus = v < 0; if (isMinus) v = -v; const i = Math.floor(Math.log(v) / Math.log(1024)); - return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; + return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/(\.[1-9]*)0+$/, '$1').replace(/\.$/, '') + (sizes[i] ?? `e+${ i * 3 }B`); }; diff --git a/packages/frontend/src/filters/kmg.ts b/packages/frontend/src/filters/kmg.ts new file mode 100644 index 0000000000..4dcb5c5800 --- /dev/null +++ b/packages/frontend/src/filters/kmg.ts @@ -0,0 +1,9 @@ +export default (v, fractionDigits = 0) => { + if (v == null) return 'N/A'; + if (v === 0) return '0'; + const sizes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q']; + const isMinus = v < 0; + if (isMinus) v = -v; + const i = Math.floor(Math.log(v) / Math.log(1000)); + return (isMinus ? '-' : '') + (v / Math.pow(1000, i)).toFixed(fractionDigits).replace(/(\.[1-9]*)0+$/, '$1').replace(/\.$/, '') + (sizes[i] ?? `e+${ i * 3 }`); +}; diff --git a/packages/frontend/src/global/router/definition.ts b/packages/frontend/src/global/router/definition.ts new file mode 100644 index 0000000000..7637ed684f --- /dev/null +++ b/packages/frontend/src/global/router/definition.ts @@ -0,0 +1,611 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue'; +import { IRouter, Router } from '@/nirax.js'; +import { $i, iAmModerator } from '@/account.js'; +import MkLoading from '@/pages/_loading_.vue'; +import MkError from '@/pages/_error_.vue'; +import { setMainRouter } from '@/global/router/main.js'; + +const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ + loader: loader, + loadingComponent: MkLoading, + errorComponent: MkError, +}); +const routes = [{ + path: '/@:initUser/pages/:initPageName/view-source', + component: page(() => import('@/pages/page-editor/page-editor.vue')), +}, { + path: '/@:username/pages/:pageName', + component: page(() => import('@/pages/page.vue')), +}, { + path: '/@:acct/following', + component: page(() => import('@/pages/user/following.vue')), +}, { + path: '/@:acct/followers', + component: page(() => import('@/pages/user/followers.vue')), +}, { + name: 'user', + path: '/@:acct/:page?', + component: page(() => import('@/pages/user/index.vue')), +}, { + name: 'note', + path: '/notes/:noteId', + component: page(() => import('@/pages/note.vue')), +}, { + name: 'list', + path: '/list/:listId', + component: page(() => import('@/pages/list.vue')), +}, { + path: '/clips/:clipId', + component: page(() => import('@/pages/clip.vue')), +}, { + path: '/instance-info/:host', + component: page(() => import('@/pages/instance-info.vue')), +}, { + name: 'settings', + path: '/settings', + component: page(() => import('@/pages/settings/index.vue')), + loginRequired: true, + children: [{ + path: '/profile', + name: 'profile', + component: page(() => import('@/pages/settings/profile.vue')), + }, { + path: '/avatar-decoration', + name: 'avatarDecoration', + component: page(() => import('@/pages/settings/avatar-decoration.vue')), + }, { + path: '/roles', + name: 'roles', + component: page(() => import('@/pages/settings/roles.vue')), + }, { + path: '/privacy', + name: 'privacy', + component: page(() => import('@/pages/settings/privacy.vue')), + }, { + path: '/emoji-picker', + name: 'emojiPicker', + component: page(() => import('@/pages/settings/emoji-picker.vue')), + }, { + path: '/drive', + name: 'drive', + component: page(() => import('@/pages/settings/drive.vue')), + }, { + path: '/drive/cleaner', + name: 'drive', + component: page(() => import('@/pages/settings/drive-cleaner.vue')), + }, { + path: '/notifications', + name: 'notifications', + component: page(() => import('@/pages/settings/notifications.vue')), + }, { + path: '/email', + name: 'email', + component: page(() => import('@/pages/settings/email.vue')), + }, { + path: '/security', + name: 'security', + component: page(() => import('@/pages/settings/security.vue')), + }, { + path: '/general', + name: 'general', + component: page(() => import('@/pages/settings/general.vue')), + }, { + path: '/theme/install', + name: 'theme', + component: page(() => import('@/pages/settings/theme.install.vue')), + }, { + path: '/theme/manage', + name: 'theme', + component: page(() => import('@/pages/settings/theme.manage.vue')), + }, { + path: '/theme', + name: 'theme', + component: page(() => import('@/pages/settings/theme.vue')), + }, { + path: '/navbar', + name: 'navbar', + component: page(() => import('@/pages/settings/navbar.vue')), + }, { + path: '/statusbar', + name: 'statusbar', + component: page(() => import('@/pages/settings/statusbar.vue')), + }, { + path: '/sounds', + name: 'sounds', + component: page(() => import('@/pages/settings/sounds.vue')), + }, { + path: '/plugin/install', + name: 'plugin', + component: page(() => import('@/pages/settings/plugin.install.vue')), + }, { + path: '/plugin', + name: 'plugin', + component: page(() => import('@/pages/settings/plugin.vue')), + }, { + path: '/import-export', + name: 'import-export', + component: page(() => import('@/pages/settings/import-export.vue')), + }, { + path: '/mute-block', + name: 'mute-block', + component: page(() => import('@/pages/settings/mute-block.vue')), + }, { + path: '/api', + name: 'api', + component: page(() => import('@/pages/settings/api.vue')), + }, { + path: '/apps', + name: 'api', + component: page(() => import('@/pages/settings/apps.vue')), + }, { + path: '/webhook/edit/:webhookId', + name: 'webhook', + component: page(() => import('@/pages/settings/webhook.edit.vue')), + }, { + path: '/webhook/new', + name: 'webhook', + component: page(() => import('@/pages/settings/webhook.new.vue')), + }, { + path: '/webhook', + name: 'webhook', + component: page(() => import('@/pages/settings/webhook.vue')), + }, { + path: '/deck', + name: 'deck', + component: page(() => import('@/pages/settings/deck.vue')), + }, { + path: '/preferences-backups', + name: 'preferences-backups', + component: page(() => import('@/pages/settings/preferences-backups.vue')), + }, { + path: '/migration', + name: 'migration', + component: page(() => import('@/pages/settings/migration.vue')), + }, { + path: '/custom-css', + name: 'general', + component: page(() => import('@/pages/settings/custom-css.vue')), + }, { + path: '/accounts', + name: 'profile', + component: page(() => import('@/pages/settings/accounts.vue')), + }, { + path: '/other', + name: 'other', + component: page(() => import('@/pages/settings/other.vue')), + }, { + path: '/', + component: page(() => import('@/pages/_empty_.vue')), + }], +}, { + path: '/reset-password/:token?', + component: page(() => import('@/pages/reset-password.vue')), +}, { + path: '/signup-complete/:code', + component: page(() => import('@/pages/signup-complete.vue')), +}, { + path: '/announcements', + component: page(() => import('@/pages/announcements.vue')), +}, { + path: '/about', + component: page(() => import('@/pages/about.vue')), + hash: 'initialTab', +}, { + path: '/about-sharkey', + component: page(() => import('@/pages/about-sharkey.vue')), +}, { + path: '/invite', + name: 'invite', + component: page(() => import('@/pages/invite.vue')), +}, { + path: '/ads', + component: page(() => import('@/pages/ads.vue')), +}, { + path: '/theme-editor', + component: page(() => import('@/pages/theme-editor.vue')), + loginRequired: true, +}, { + path: '/roles/:role', + component: page(() => import('@/pages/role.vue')), +}, { + path: '/user-tags/:tag', + component: page(() => import('@/pages/user-tag.vue')), +}, { + path: '/explore', + component: page(() => import('@/pages/explore.vue')), + hash: 'initialTab', +}, { + path: '/search', + component: page(() => import('@/pages/search.vue')), + query: { + q: 'query', + channel: 'channel', + type: 'type', + origin: 'origin', + }, +}, { + path: '/authorize-follow', + component: page(() => import('@/pages/follow.vue')), + loginRequired: true, +}, { + path: '/share', + component: page(() => import('@/pages/share.vue')), + loginRequired: true, +}, { + path: '/api-console', + component: page(() => import('@/pages/api-console.vue')), + loginRequired: true, +}, { + path: '/scratchpad', + component: page(() => import('@/pages/scratchpad.vue')), +}, { + path: '/auth/:token', + component: page(() => import('@/pages/auth.vue')), +}, { + path: '/miauth/:session', + component: page(() => import('@/pages/miauth.vue')), + query: { + callback: 'callback', + name: 'name', + icon: 'icon', + permission: 'permission', + }, +}, { + path: '/oauth/authorize', + component: page(() => import('@/pages/oauth.vue')), +}, { + path: '/tags/:tag', + component: page(() => import('@/pages/tag.vue')), +}, { + path: '/pages/new', + component: page(() => import('@/pages/page-editor/page-editor.vue')), + loginRequired: true, +}, { + path: '/pages/edit/:initPageId', + component: page(() => import('@/pages/page-editor/page-editor.vue')), + loginRequired: true, +}, { + path: '/pages', + component: page(() => import('@/pages/pages.vue')), +}, { + path: '/play/:id/edit', + component: page(() => import('@/pages/flash/flash-edit.vue')), + loginRequired: true, +}, { + path: '/play/new', + component: page(() => import('@/pages/flash/flash-edit.vue')), + loginRequired: true, +}, { + path: '/play/:id', + component: page(() => import('@/pages/flash/flash.vue')), +}, { + path: '/play', + component: page(() => import('@/pages/flash/flash-index.vue')), +}, { + path: '/gallery/:postId/edit', + component: page(() => import('@/pages/gallery/edit.vue')), + loginRequired: true, +}, { + path: '/gallery/new', + component: page(() => import('@/pages/gallery/edit.vue')), + loginRequired: true, +}, { + path: '/gallery/:postId', + component: page(() => import('@/pages/gallery/post.vue')), +}, { + path: '/gallery', + component: page(() => import('@/pages/gallery/index.vue')), +}, { + path: '/channels/:channelId/edit', + component: page(() => import('@/pages/channel-editor.vue')), + loginRequired: true, +}, { + path: '/channels/new', + component: page(() => import('@/pages/channel-editor.vue')), + loginRequired: true, +}, { + path: '/channels/:channelId', + component: page(() => import('@/pages/channel.vue')), +}, { + path: '/channels', + component: page(() => import('@/pages/channels.vue')), +}, { + path: '/custom-emojis-manager', + component: page(() => import('@/pages/custom-emojis-manager.vue')), +}, { + path: '/avatar-decorations', + name: 'avatarDecorations', + component: page(() => import('@/pages/avatar-decorations.vue')), +}, { + path: '/registry/keys/:domain/:path(*)?', + component: page(() => import('@/pages/registry.keys.vue')), +}, { + path: '/registry/value/:domain/:path(*)?', + component: page(() => import('@/pages/registry.value.vue')), +}, { + path: '/registry', + component: page(() => import('@/pages/registry.vue')), +}, { + path: '/install-extentions', + component: page(() => import('@/pages/install-extentions.vue')), + loginRequired: true, +}, { + path: '/admin/user/:userId', + component: iAmModerator ? page(() => import('@/pages/admin-user.vue')) : page(() => import('@/pages/not-found.vue')), +}, { + path: '/admin/file/:fileId', + component: iAmModerator ? page(() => import('@/pages/admin-file.vue')) : page(() => import('@/pages/not-found.vue')), +}, { + path: '/admin', + component: iAmModerator ? page(() => import('@/pages/admin/index.vue')) : page(() => import('@/pages/not-found.vue')), + children: [{ + path: '/overview', + name: 'overview', + component: page(() => import('@/pages/admin/overview.vue')), + }, { + path: '/users', + name: 'users', + component: page(() => import('@/pages/admin/users.vue')), + }, { + path: '/emojis', + name: 'emojis', + component: page(() => import('@/pages/custom-emojis-manager.vue')), + }, { + path: '/avatar-decorations', + name: 'avatarDecorations', + component: page(() => import('@/pages/avatar-decorations.vue')), + }, { + path: '/queue', + name: 'queue', + component: page(() => import('@/pages/admin/queue.vue')), + }, { + path: '/files', + name: 'files', + component: page(() => import('@/pages/admin/files.vue')), + }, { + path: '/federation', + name: 'federation', + component: page(() => import('@/pages/admin/federation.vue')), + }, { + path: '/announcements', + name: 'announcements', + component: page(() => import('@/pages/admin/announcements.vue')), + }, { + path: '/ads', + name: 'ads', + component: page(() => import('@/pages/admin/ads.vue')), + }, { + path: '/roles/:id/edit', + name: 'roles', + component: page(() => import('@/pages/admin/roles.edit.vue')), + }, { + path: '/roles/new', + name: 'roles', + component: page(() => import('@/pages/admin/roles.edit.vue')), + }, { + path: '/roles/:id', + name: 'roles', + component: page(() => import('@/pages/admin/roles.role.vue')), + }, { + path: '/roles', + name: 'roles', + component: page(() => import('@/pages/admin/roles.vue')), + }, { + path: '/database', + name: 'database', + component: page(() => import('@/pages/admin/database.vue')), + }, { + path: '/abuses', + name: 'abuses', + component: page(() => import('@/pages/admin/abuses.vue')), + }, { + path: '/modlog', + name: 'modlog', + component: page(() => import('@/pages/admin/modlog.vue')), + }, { + path: '/settings', + name: 'settings', + component: page(() => import('@/pages/admin/settings.vue')), + }, { + path: '/branding', + name: 'branding', + component: page(() => import('@/pages/admin/branding.vue')), + }, { + path: '/moderation', + name: 'moderation', + component: page(() => import('@/pages/admin/moderation.vue')), + }, { + path: '/email-settings', + name: 'email-settings', + component: page(() => import('@/pages/admin/email-settings.vue')), + }, { + path: '/object-storage', + name: 'object-storage', + component: page(() => import('@/pages/admin/object-storage.vue')), + }, { + path: '/security', + name: 'security', + component: page(() => import('@/pages/admin/security.vue')), + }, { + path: '/relays', + name: 'relays', + component: page(() => import('@/pages/admin/relays.vue')), + }, { + path: '/instance-block', + name: 'instance-block', + component: page(() => import('@/pages/admin/instance-block.vue')), + }, { + path: '/proxy-account', + name: 'proxy-account', + component: page(() => import('@/pages/admin/proxy-account.vue')), + }, { + path: '/external-services', + name: 'external-services', + component: page(() => import('@/pages/admin/external-services.vue')), + }, { + path: '/other-settings', + name: 'other-settings', + component: page(() => import('@/pages/admin/other-settings.vue')), + }, { + path: '/server-rules', + name: 'server-rules', + component: page(() => import('@/pages/admin/server-rules.vue')), + }, { + path: '/invites', + name: 'invites', + component: page(() => import('@/pages/admin/invites.vue')), + }, { + path: '/approvals', + name: 'approvals', + component: page(() => import('@/pages/admin/approvals.vue')), + }, { + path: '/', + component: page(() => import('@/pages/_empty_.vue')), + }], +}, { + path: '/my/notifications', + component: page(() => import('@/pages/notifications.vue')), + loginRequired: true, +}, { + path: '/my/favorites', + component: page(() => import('@/pages/favorites.vue')), + loginRequired: true, +}, { + path: '/my/achievements', + component: page(() => import('@/pages/achievements.vue')), + loginRequired: true, +}, { + path: '/my/drive/folder/:folder', + component: page(() => import('@/pages/drive.vue')), + loginRequired: true, +}, { + path: '/my/drive', + component: page(() => import('@/pages/drive.vue')), + loginRequired: true, +}, { + path: '/my/drive/file/:fileId', + component: page(() => import('@/pages/drive.file.vue')), + loginRequired: true, +}, { + path: '/my/follow-requests', + component: page(() => import('@/pages/follow-requests.vue')), + loginRequired: true, +}, { + path: '/my/lists/:listId', + component: page(() => import('@/pages/my-lists/list.vue')), + loginRequired: true, +}, { + path: '/my/lists', + component: page(() => import('@/pages/my-lists/index.vue')), + loginRequired: true, +}, { + path: '/my/clips', + component: page(() => import('@/pages/my-clips/index.vue')), + loginRequired: true, +}, { + path: '/my/antennas/create', + component: page(() => import('@/pages/my-antennas/create.vue')), + loginRequired: true, +}, { + path: '/my/antennas/:antennaId', + component: page(() => import('@/pages/my-antennas/edit.vue')), + loginRequired: true, +}, { + path: '/my/antennas', + component: page(() => import('@/pages/my-antennas/index.vue')), + loginRequired: true, +}, { + path: '/timeline/list/:listId', + component: page(() => import('@/pages/user-list-timeline.vue')), + loginRequired: true, +}, { + path: '/timeline/antenna/:antennaId', + component: page(() => import('@/pages/antenna-timeline.vue')), + loginRequired: true, +}, { + path: '/clicker', + component: page(() => import('@/pages/clicker.vue')), + loginRequired: true, +}, { + path: '/bubble-game', + component: page(() => import('@/pages/drop-and-fusion.vue')), + loginRequired: true, +}, { + path: '/timeline', + component: page(() => import('@/pages/timeline.vue')), +}, { + name: 'index', + path: '/', + component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')), + globalCacheKey: 'index', +}, { + path: '/:(*)', + component: page(() => import('@/pages/not-found.vue')), +}]; + +function createRouterImpl(path: string): IRouter { + return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue'))); +} + +/** + * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。 + * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能) + */ +export function setupRouter(app: App) { + app.provide('routerFactory', createRouterImpl); + + const mainRouter = createRouterImpl(location.pathname + location.search + location.hash); + + window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href); + + const scrollPosStore = new Map<string, number>(); + let restoring = false; + + window.setInterval(() => { + if (!restoring) { + scrollPosStore.set(window.history.state?.key, window.scrollY); + } + }, 1000); + + window.addEventListener('popstate', (event) => { + mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); + + restoring = true; + const scrollPos = scrollPosStore.get(event.state?.key) ?? 0; + window.scroll({ top: scrollPos, behavior: 'instant' }); + window.setTimeout(() => { + // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール + window.scroll({ top: scrollPos, behavior: 'instant' }); + restoring = false; + }, 100); + }); + + mainRouter.addListener('push', ctx => { + window.history.pushState({ key: ctx.key }, '', ctx.path); + + restoring = true; + const scrollPos = scrollPosStore.get(ctx.key) ?? 0; + window.scroll({ top: scrollPos, behavior: 'instant' }); + + if (scrollPos !== 0) { + window.setTimeout(() => { + // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール + window.scroll({ top: scrollPos, behavior: 'instant' }); + }, 100); + restoring = false; + } else { + restoring = false; + } + }); + + mainRouter.addListener('same', () => { + window.scroll({ top: 0, behavior: 'smooth' }); + }); + + setMainRouter(mainRouter); +} diff --git a/packages/frontend/src/global/router/main.ts b/packages/frontend/src/global/router/main.ts new file mode 100644 index 0000000000..5adb3f606f --- /dev/null +++ b/packages/frontend/src/global/router/main.ts @@ -0,0 +1,163 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ShallowRef } from 'vue'; +import { EventEmitter } from 'eventemitter3'; +import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js'; + +function getMainRouter(): IRouter { + const router = mainRouterHolder; + if (!router) { + throw new Error('mainRouter is not found.'); + } + + return router; +} + +/** + * メインルータを設定する。一度設定すると、それ以降は変更できない。 + * {@link setupRouter}から呼び出されることのみを想定している。 + */ +export function setMainRouter(router: IRouter) { + if (mainRouterHolder) { + throw new Error('mainRouter is already exists.'); + } + + mainRouterHolder = router; +} + +/** + * {@link mainRouter}用のプロキシ実装。 + * {@link mainRouter}は起動シーケンスの一部にて初期化されるため、僅かにundefinedになる期間がある。 + * その僅かな期間のためだけに型をundefined込みにしたくないのでこのクラスを緩衝材として使用する。 + */ +class MainRouterProxy implements IRouter { + private supplier: () => IRouter; + + constructor(supplier: () => IRouter) { + this.supplier = supplier; + } + + get current(): Resolved { + return this.supplier().current; + } + + get currentRef(): ShallowRef<Resolved> { + return this.supplier().currentRef; + } + + get currentRoute(): ShallowRef<RouteDef> { + return this.supplier().currentRoute; + } + + get navHook(): ((path: string, flag?: any) => boolean) | null { + return this.supplier().navHook; + } + + set navHook(value) { + this.supplier().navHook = value; + } + + getCurrentKey(): string { + return this.supplier().getCurrentKey(); + } + + getCurrentPath(): any { + return this.supplier().getCurrentPath(); + } + + push(path: string, flag?: any): void { + this.supplier().push(path, flag); + } + + replace(path: string, key?: string | null): void { + this.supplier().replace(path, key); + } + + resolve(path: string): Resolved | null { + return this.supplier().resolve(path); + } + + eventNames(): Array<EventEmitter.EventNames<RouterEvent>> { + return this.supplier().eventNames(); + } + + listeners<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + ): Array<EventEmitter.EventListener<RouterEvent, T>> { + return this.supplier().listeners(event); + } + + listenerCount( + event: EventEmitter.EventNames<RouterEvent>, + ): number { + return this.supplier().listenerCount(event); + } + + emit<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + ...args: EventEmitter.EventArgs<RouterEvent, T> + ): boolean { + return this.supplier().emit(event, ...args); + } + + on<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + ): this { + this.supplier().on(event, fn, context); + return this; + } + + addListener<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + ): this { + this.supplier().addListener(event, fn, context); + return this; + } + + once<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + ): this { + this.supplier().once(event, fn, context); + return this; + } + + removeListener<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn?: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + once?: boolean, + ): this { + this.supplier().removeListener(event, fn, context, once); + return this; + } + + off<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn?: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + once?: boolean, + ): this { + this.supplier().off(event, fn, context, once); + return this; + } + + removeAllListeners( + event?: EventEmitter.EventNames<RouterEvent>, + ): this { + this.supplier().removeAllListeners(event); + return this; + } +} + +let mainRouterHolder: IRouter | null = null; + +export const mainRouter: IRouter = new MainRouterProxy(getMainRouter); diff --git a/packages/frontend/src/global/router/supplier.ts b/packages/frontend/src/global/router/supplier.ts new file mode 100644 index 0000000000..1e321ef21f --- /dev/null +++ b/packages/frontend/src/global/router/supplier.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { inject } from 'vue'; +import { IRouter, Router } from '@/nirax.js'; +import { mainRouter } from '@/global/router/main.js'; + +/** + * メインの{@link Router}を取得する。 + * あらかじめ{@link setupRouter}を実行しておく必要がある({@link provide}により{@link IRouter}のインスタンスを注入可能であるならばこの限りではない) + */ +export function useRouter(): IRouter { + return inject<Router | null>('router', null) ?? mainRouter; +} + +/** + * 任意の{@link Router}を取得するためのファクトリを取得する。 + * あらかじめ{@link setupRouter}を実行しておく必要がある。 + */ +export function useRouterFactory(): (path: string) => IRouter { + const factory = inject<(path: string) => IRouter>('routerFactory'); + if (!factory) { + console.error('routerFactory is not defined.'); + throw new Error('routerFactory is not defined.'); + } + + return factory; +} diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html index 8de01e4802..11555ea18a 100644 --- a/packages/frontend/src/index.html +++ b/packages/frontend/src/index.html @@ -16,13 +16,13 @@ <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <meta http-equiv="Content-Security-Policy" - content="default-src 'self'; + content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/; worker-src 'self'; - script-src 'self' 'unsafe-eval'; + script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; - img-src 'self' data: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; + img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; - connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;" + connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;" /> <meta property="og:site_name" content="[DEV BUILD] Misskey" /> <meta name="viewport" content="width=device-width, initial-scale=1"> diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts index b09264dabb..739e90101b 100644 --- a/packages/frontend/src/instance.ts +++ b/packages/frontend/src/instance.ts @@ -5,7 +5,7 @@ import { computed, reactive } from 'vue'; import * as Misskey from 'misskey-js'; -import { api } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { miLocalStorage } from '@/local-storage.js'; import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@/const.js'; @@ -26,7 +26,7 @@ export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL); export async function fetchInstance() { - const meta = await api('meta', { + const meta = await misskeyApi('meta', { detail: false, }); diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts index 9564a754f0..587f88412d 100644 --- a/packages/frontend/src/nirax.ts +++ b/packages/frontend/src/nirax.ts @@ -5,11 +5,11 @@ // NIRAX --- A lightweight router -import { EventEmitter } from 'eventemitter3'; import { Component, onMounted, shallowRef, ShallowRef } from 'vue'; +import { EventEmitter } from 'eventemitter3'; import { safeURIDecode } from '@/scripts/safe-uri-decode.js'; -type RouteDef = { +export type RouteDef = { path: string; component: Component; query?: Record<string, string>; @@ -27,6 +27,27 @@ type ParsedPath = (string | { optional?: boolean; })[]; +export type RouterEvent = { + change: (ctx: { + beforePath: string; + path: string; + resolved: Resolved; + key: string; + }) => void; + replace: (ctx: { + path: string; + key: string; + }) => void; + push: (ctx: { + beforePath: string; + path: string; + route: RouteDef | null; + props: Map<string, string> | null; + key: string; + }) => void; + same: () => void; +} + export type Resolved = { route: RouteDef; props: Map<string, string | boolean>; child?: Resolved; }; function parsePath(path: string): ParsedPath { @@ -54,26 +75,85 @@ function parsePath(path: string): ParsedPath { return res; } -export class Router extends EventEmitter<{ - change: (ctx: { - beforePath: string; - path: string; - resolved: Resolved; - key: string; - }) => void; - replace: (ctx: { - path: string; - key: string; - }) => void; - push: (ctx: { - beforePath: string; - path: string; - route: RouteDef | null; - props: Map<string, string> | null; - key: string; - }) => void; - same: () => void; -}> { +export interface IRouter extends EventEmitter<RouterEvent> { + current: Resolved; + currentRef: ShallowRef<Resolved>; + currentRoute: ShallowRef<RouteDef>; + navHook: ((path: string, flag?: any) => boolean) | null; + + resolve(path: string): Resolved | null; + + getCurrentPath(): any; + + getCurrentKey(): string; + + push(path: string, flag?: any): void; + + replace(path: string, key?: string | null): void; + + /** @see EventEmitter */ + eventNames(): Array<EventEmitter.EventNames<RouterEvent>>; + + /** @see EventEmitter */ + listeners<T extends EventEmitter.EventNames<RouterEvent>>( + event: T + ): Array<EventEmitter.EventListener<RouterEvent, T>>; + + /** @see EventEmitter */ + listenerCount( + event: EventEmitter.EventNames<RouterEvent> + ): number; + + /** @see EventEmitter */ + emit<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + ...args: EventEmitter.EventArgs<RouterEvent, T> + ): boolean; + + /** @see EventEmitter */ + on<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn: EventEmitter.EventListener<RouterEvent, T>, + context?: any + ): this; + + /** @see EventEmitter */ + addListener<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn: EventEmitter.EventListener<RouterEvent, T>, + context?: any + ): this; + + /** @see EventEmitter */ + once<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn: EventEmitter.EventListener<RouterEvent, T>, + context?: any + ): this; + + /** @see EventEmitter */ + removeListener<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn?: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + once?: boolean | undefined + ): this; + + /** @see EventEmitter */ + off<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn?: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + once?: boolean | undefined + ): this; + + /** @see EventEmitter */ + removeAllListeners( + event?: EventEmitter.EventNames<RouterEvent> + ): this; +} + +export class Router extends EventEmitter<RouterEvent> implements IRouter { private routes: RouteDef[]; public current: Resolved; public currentRef: ShallowRef<Resolved> = shallowRef(); @@ -277,7 +357,7 @@ export class Router extends EventEmitter<{ } } -export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: Router) { +export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: IRouter) { const scrollPosStore = new Map<string, number>(); onMounted(() => { diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index b02f6aa640..a63d61bb8f 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -5,12 +5,11 @@ // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する -import { pendingApiRequestsCount, api, apiGet } from '@/scripts/api.js'; -export { pendingApiRequestsCount, api, apiGet }; import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue'; import { EventEmitter } from 'eventemitter3'; import insertTextAtCursor from 'insert-text-at-cursor'; import * as Misskey from 'misskey-js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import MkPostFormDialog from '@/components/MkPostFormDialog.vue'; import MkWaitingDialog from '@/components/MkWaitingDialog.vue'; @@ -33,7 +32,7 @@ export const apiWithDialog = (( data: Record<string, any> = {}, token?: string | null | undefined, ) => { - const promise = api(endpoint, data, token); + const promise = misskeyApi(endpoint, data, token); promiseDialog(promise, null, async (err) => { let title = null; let text = err.message + '\n' + (err as any).id; @@ -83,7 +82,7 @@ export const apiWithDialog = (( }); return promise; -}) as typeof api; +}) as typeof misskeyApi; export function promiseDialog<T extends Promise<any>>( promise: T, @@ -621,7 +620,7 @@ export function checkExistence(fileData: ArrayBuffer): Promise<any> { const data = new FormData(); data.append('md5', getMD5(fileData)); - os.api('drive/files/find-by-hash', { + api('drive/files/find-by-hash', { md5: getMD5(fileData) }).then(resp => { resolve(resp.length > 0 ? resp[0] : null); diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue index 2cdf8f2e8c..1165b026f5 100644 --- a/packages/frontend/src/pages/_error_.vue +++ b/packages/frontend/src/pages/_error_.vue @@ -29,7 +29,7 @@ import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import { version } from '@/config.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -46,7 +46,7 @@ const loaded = ref(false); const serverIsDead = ref(false); const meta = ref<Misskey.entities.MetaResponse | null>(null); -os.api('meta', { +misskeyApi('meta', { detail: false, }).then(res => { loaded.value = true; diff --git a/packages/frontend/src/pages/about-sharkey.vue b/packages/frontend/src/pages/about-sharkey.vue index 2e4ff5d041..f29d2e4916 100644 --- a/packages/frontend/src/pages/about-sharkey.vue +++ b/packages/frontend/src/pages/about-sharkey.vue @@ -120,6 +120,7 @@ import { physics } from '@/scripts/physics.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; import { $i } from '@/account.js'; @@ -132,7 +133,7 @@ const easterEggEngine = ref(null); const sponsors = ref([]); const containerEl = shallowRef<HTMLElement>(); -await os.api('sponsors', { forceUpdate: true }).then((res) => sponsors.value.push(res.sponsor_data)); +await misskeyApi('sponsors', { forceUpdate: true }).then((res) => sponsors.value.push(res.sponsor_data)); function iconLoaded() { const emojis = defaultStore.state.reactions; diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index b532314745..8209515065 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -114,7 +114,7 @@ import FormSplit from '@/components/form/split.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkInstanceStats from '@/components/MkInstanceStats.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import number from '@/filters/number.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -136,7 +136,7 @@ watch(tab, () => { } }); -const initStats = () => os.api('stats', { +const initStats = () => misskeyApi('stats', { }).then((res) => { stats.value = res; }); diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index 845beebbaf..313622a289 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -79,6 +79,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkInfo from '@/components/MkInfo.vue'; import bytes from '@/filters/bytes.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { iAmAdmin, iAmModerator } from '@/account.js'; @@ -93,8 +94,8 @@ const props = defineProps<{ }>(); async function fetch() { - file.value = await os.api('drive/files/show', { fileId: props.fileId }); - info.value = await os.api('admin/drive/show-file', { fileId: props.fileId }); + file.value = await misskeyApi('drive/files/show', { fileId: props.fileId }); + info.value = await misskeyApi('admin/drive/show-file', { fileId: props.fileId }); isSensitive.value = file.value.isSensitive; } @@ -113,7 +114,7 @@ async function del() { } async function toggleIsSensitive(v) { - await os.api('drive/files/update', { fileId: props.fileId, isSensitive: v }); + await misskeyApi('drive/files/update', { fileId: props.fileId, isSensitive: v }); isSensitive.value = v; } diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 741897b5f0..7d2607fe9d 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -206,11 +206,12 @@ import FormSuspense from '@/components/form/suspense.vue'; import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue'; import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { url } from '@/config.js'; import { acct } from '@/filters/user.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; -import { iAmAdmin, $i } from '@/account.js'; +import { iAmAdmin, $i, iAmModerator } from '@/account.js'; import MkRolePreview from '@/components/MkRolePreview.vue'; import MkPagination from '@/components/MkPagination.vue'; @@ -251,11 +252,11 @@ const announcementsPagination = { const expandedRoles = ref([]); function createFetcher() { - return () => Promise.all([os.api('users/show', { + return () => Promise.all([misskeyApi('users/show', { userId: props.userId, - }), os.api('admin/show-user', { + }), misskeyApi('admin/show-user', { userId: props.userId, - }), iAmAdmin ? os.api('admin/get-user-ips', { + }), iAmAdmin ? misskeyApi('admin/get-user-ips', { userId: props.userId, }) : Promise.resolve(null)]).then(([_user, _info, _ips]) => { user.value = _user; @@ -268,7 +269,7 @@ function createFetcher() { moderationNote.value = info.value.moderationNote; watch(moderationNote, async () => { - await os.api('admin/update-user-note', { userId: user.value.id, text: moderationNote.value }); + await misskeyApi('admin/update-user-note', { userId: user.value.id, text: moderationNote.value }); await refreshUser(); }); }); @@ -291,7 +292,7 @@ async function resetPassword() { if (confirm.canceled) { return; } else { - const { password } = await os.api('admin/reset-password', { + const { password } = await misskeyApi('admin/reset-password', { userId: user.value.id, }); os.alert({ @@ -309,7 +310,7 @@ async function toggleNSFW(v) { if (confirm.canceled) { markedAsNSFW.value = !v; } else { - await os.api(v ? 'admin/nsfw-user' : 'admin/unnsfw-user', { userId: user.value.id }); + await misskeyApi(v ? 'admin/nsfw-user' : 'admin/unnsfw-user', { userId: user.value.id }); await refreshUser(); } } @@ -322,7 +323,7 @@ async function toggleSilence(v) { if (confirm.canceled) { silenced.value = !v; } else { - await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: user.value.id }); + await misskeyApi(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: user.value.id }); await refreshUser(); } } @@ -335,7 +336,7 @@ async function toggleSuspend(v) { if (confirm.canceled) { suspended.value = !v; } else { - await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: user.value.id }); + await misskeyApi(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: user.value.id }); await refreshUser(); } } @@ -347,7 +348,7 @@ async function unsetUserAvatar() { }); if (confirm.canceled) return; const process = async () => { - await os.api('admin/unset-user-avatar', { userId: user.value.id }); + await misskeyApi('admin/unset-user-avatar', { userId: user.value.id }); os.success(); }; await process().catch(err => { @@ -366,7 +367,7 @@ async function unsetUserBanner() { }); if (confirm.canceled) return; const process = async () => { - await os.api('admin/unset-user-banner', { userId: user.value.id }); + await misskeyApi('admin/unset-user-banner', { userId: user.value.id }); os.success(); }; await process().catch(err => { @@ -385,7 +386,7 @@ async function deleteAllFiles() { }); if (confirm.canceled) return; const process = async () => { - await os.api('admin/delete-all-files-of-a-user', { userId: user.value.id }); + await misskeyApi('admin/delete-all-files-of-a-user', { userId: user.value.id }); os.success(); }; await process().catch(err => { @@ -422,7 +423,7 @@ async function deleteAccount() { } async function assignRole() { - const roles = await os.api('admin/roles/list'); + const roles = await misskeyApi('admin/roles/list'); const { canceled, result: roleId } = await os.select({ title: i18n.ts._role.chooseRoleToAssign, @@ -498,7 +499,7 @@ watch(() => props.userId, () => { }); watch(user, () => { - os.api('ap/get', { + misskeyApi('ap/get', { uri: user.value.uri ?? `${url}/users/${user.value.id}`, }).then(res => { ap.value = res; diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index 8a1e03c30d..2d261fc458 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -96,6 +96,7 @@ import MkFolder from '@/components/MkFolder.vue'; import MkSelect from '@/components/MkSelect.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -108,7 +109,7 @@ const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, const filterType = ref('all'); let publishing: boolean | null = null; -os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => { +misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => { if (adsResponse != null) { ads.value = adsResponse.map(r => { const exdate = new Date(r.expiresAt); @@ -174,7 +175,7 @@ function remove(ad) { function save(ad) { if (ad.id == null) { - os.api('admin/ad/create', { + misskeyApi('admin/ad/create', { ...ad, expiresAt: new Date(ad.expiresAt).getTime(), startsAt: new Date(ad.startsAt).getTime(), @@ -191,7 +192,7 @@ function save(ad) { }); }); } else { - os.api('admin/ad/update', { + misskeyApi('admin/ad/update', { ...ad, expiresAt: new Date(ad.expiresAt).getTime(), startsAt: new Date(ad.startsAt).getTime(), @@ -210,7 +211,7 @@ function save(ad) { } function more() { - os.api('admin/ad/list', { untilId: ads.value.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => { + misskeyApi('admin/ad/list', { untilId: ads.value.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => { if (adsResponse == null) return; ads.value = ads.value.concat(adsResponse.map(r => { const exdate = new Date(r.expiresAt); @@ -227,7 +228,7 @@ function more() { } function refresh() { - os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => { + misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => { if (adsResponse == null) return; ads.value = adsResponse.map(r => { const exdate = new Date(r.expiresAt); diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index 931bd9bbc8..21c323e79d 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -79,6 +79,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkFolder from '@/components/MkFolder.vue'; @@ -86,7 +87,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; const announcements = ref<any[]>([]); -os.api('admin/announcements/list').then(announcementResponse => { +misskeyApi('admin/announcements/list').then(announcementResponse => { announcements.value = announcementResponse; }); @@ -112,7 +113,7 @@ function del(announcement) { }).then(({ canceled }) => { if (canceled) return; announcements.value = announcements.value.filter(x => x !== announcement); - os.api('admin/announcements/delete', announcement); + misskeyApi('admin/announcements/delete', announcement); }); } @@ -134,13 +135,13 @@ async function save(announcement) { } function more() { - os.api('admin/announcements/list', { untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id }).then(announcementResponse => { + misskeyApi('admin/announcements/list', { untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id }).then(announcementResponse => { announcements.value = announcements.value.concat(announcementResponse); }); } function refresh() { - os.api('admin/announcements/list').then(announcementResponse => { + misskeyApi('admin/announcements/list').then(announcementResponse => { announcements.value = announcementResponse; }); } diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index eebea51bf1..080a81767b 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkRadios v-model="provider"> <option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option> <option value="hcaptcha">hCaptcha</option> + <option value="mcaptcha">mCaptcha</option> <option value="recaptcha">reCAPTCHA</option> <option value="turnstile">Turnstile</option> </MkRadios> @@ -28,6 +29,24 @@ SPDX-License-Identifier: AGPL-3.0-only <MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/> </FormSlot> </template> + <template v-else-if="provider === 'mcaptcha'"> + <MkInput v-model="mcaptchaSiteKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.mcaptchaSiteKey }}</template> + </MkInput> + <MkInput v-model="mcaptchaSecretKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.mcaptchaSecretKey }}</template> + </MkInput> + <MkInput v-model="mcaptchaInstanceUrl"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template> + </MkInput> + <FormSlot v-if="mcaptchaSiteKey && mcaptchaInstanceUrl"> + <template #label>{{ i18n.ts.preview }}</template> + <MkCaptcha provider="mcaptcha" :sitekey="mcaptchaSiteKey" :instanceUrl="mcaptchaInstanceUrl"/> + </FormSlot> + </template> <template v-else-if="provider === 'recaptcha'"> <MkInput v-model="recaptchaSiteKey"> <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> @@ -72,6 +91,7 @@ import MkButton from '@/components/MkButton.vue'; import FormSuspense from '@/components/form/suspense.vue'; import FormSlot from '@/components/form/slot.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; @@ -80,21 +100,30 @@ const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue' const provider = ref<CaptchaProvider | null>(null); const hcaptchaSiteKey = ref<string | null>(null); const hcaptchaSecretKey = ref<string | null>(null); +const mcaptchaSiteKey = ref<string | null>(null); +const mcaptchaSecretKey = ref<string | null>(null); +const mcaptchaInstanceUrl = ref<string | null>(null); const recaptchaSiteKey = ref<string | null>(null); const recaptchaSecretKey = ref<string | null>(null); const turnstileSiteKey = ref<string | null>(null); const turnstileSecretKey = ref<string | null>(null); async function init() { - const meta = await os.api('admin/meta'); + const meta = await misskeyApi('admin/meta'); hcaptchaSiteKey.value = meta.hcaptchaSiteKey; hcaptchaSecretKey.value = meta.hcaptchaSecretKey; + mcaptchaSiteKey.value = meta.mcaptchaSiteKey; + mcaptchaSecretKey.value = meta.mcaptchaSecretKey; + mcaptchaInstanceUrl.value = meta.mcaptchaInstanceUrl; recaptchaSiteKey.value = meta.recaptchaSiteKey; recaptchaSecretKey.value = meta.recaptchaSecretKey; turnstileSiteKey.value = meta.turnstileSiteKey; turnstileSecretKey.value = meta.turnstileSecretKey; - provider.value = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : meta.enableTurnstile ? 'turnstile' : null; + provider.value = meta.enableHcaptcha ? 'hcaptcha' : + meta.enableRecaptcha ? 'recaptcha' : + meta.enableTurnstile ? 'turnstile' : + meta.enableMcaptcha ? 'mcaptcha' : null; } function save() { @@ -102,6 +131,10 @@ function save() { enableHcaptcha: provider.value === 'hcaptcha', hcaptchaSiteKey: hcaptchaSiteKey.value, hcaptchaSecretKey: hcaptchaSecretKey.value, + enableMcaptcha: provider.value === 'mcaptcha', + mcaptchaSiteKey: mcaptchaSiteKey.value, + mcaptchaSecretKey: mcaptchaSecretKey.value, + mcaptchaInstanceUrl: mcaptchaInstanceUrl.value, enableRecaptcha: provider.value === 'recaptcha', recaptchaSiteKey: recaptchaSiteKey.value, recaptchaSecretKey: recaptchaSecretKey.value, diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index fc6a9e0d67..ad82489708 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -109,6 +109,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; import FromSlot from '@/components/form/slot.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { instance, fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -131,7 +132,7 @@ const notFoundImageUrl = ref<string | null>(null); const manifestJsonOverride = ref<string>('{}'); async function init() { - const meta = await os.api('admin/meta'); + const meta = await misskeyApi('admin/meta'); iconUrl.value = meta.iconUrl; app192IconUrl.value = meta.app192IconUrl; app512IconUrl.value = meta.app512IconUrl; diff --git a/packages/frontend/src/pages/admin/database.vue b/packages/frontend/src/pages/admin/database.vue index d9fc672fbf..3dac6b3f1b 100644 --- a/packages/frontend/src/pages/admin/database.vue +++ b/packages/frontend/src/pages/admin/database.vue @@ -21,13 +21,13 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed } from 'vue'; import FormSuspense from '@/components/form/suspense.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import bytes from '@/filters/bytes.js'; import number from '@/filters/number.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)); +const databasePromiseFactory = () => misskeyApi('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)); const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue index 819619df90..ff3185aec7 100644 --- a/packages/frontend/src/pages/admin/email-settings.vue +++ b/packages/frontend/src/pages/admin/email-settings.vue @@ -73,6 +73,7 @@ import FormSuspense from '@/components/form/suspense.vue'; import FormSplit from '@/components/form/split.vue'; import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance, instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -87,7 +88,7 @@ const smtpUser = ref<string>(''); const smtpPass = ref<string>(''); async function init() { - const meta = await os.api('admin/meta'); + const meta = await misskeyApi('admin/meta'); enableEmail.value = meta.enableEmail; email.value = meta.email; smtpSecure.value = meta.smtpSecure; diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue index f4359270b6..1a67a668de 100644 --- a/packages/frontend/src/pages/admin/external-services.vue +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -42,6 +42,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import FormSuspense from '@/components/form/suspense.vue'; import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -50,7 +51,7 @@ const deeplAuthKey = ref<string>(''); const deeplIsPro = ref<boolean>(false); async function init() { - const meta = await os.api('admin/meta'); + const meta = await misskeyApi('admin/meta'); deeplAuthKey.value = meta.deeplAuthKey; deeplIsPro.value = meta.deeplIsPro; } diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue index 6808da6088..bcdd2c50a7 100644 --- a/packages/frontend/src/pages/admin/files.vue +++ b/packages/frontend/src/pages/admin/files.vue @@ -42,6 +42,7 @@ import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -83,7 +84,7 @@ async function find() { }); if (canceled) return; - os.api('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => { + misskeyApi('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => { show(file); }).catch(err => { if (err.code === 'NO_SUCH_FILE') { diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 1b41a48cb4..ab6f3007c0 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -35,9 +35,10 @@ import MkSuperMenu from '@/components/MkSuperMenu.vue'; import MkInfo from '@/components/MkInfo.vue'; import { instance } from '@/instance.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js'; -import { useRouter } from '@/router.js'; import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; +import { useRouter } from '@/global/router/supplier.js'; const isEmpty = (x: string | null) => x == null || x === ''; @@ -64,14 +65,14 @@ const thereIsUnresolvedAbuseReport = ref(false); const pendingUserApprovals = ref(false); const currentPage = computed(() => router.currentRef.value.child); -os.api('admin/abuse-user-reports', { +misskeyApi('admin/abuse-user-reports', { state: 'unresolved', limit: 1, }).then(reports => { if (reports.length > 0) thereIsUnresolvedAbuseReport.value = true; }); -os.api('admin/show-users', { +misskeyApi('admin/show-users', { state: 'approved', origin: 'local', limit: 1, @@ -281,7 +282,7 @@ provideMetadataReceiver((info) => { }); function invite() { - os.api('admin/invite/create').then(x => { + misskeyApi('admin/invite/create').then(x => { os.alert({ type: 'info', text: x[0].code, diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue index e54f6dc065..b01b9bd388 100644 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ b/packages/frontend/src/pages/admin/instance-block.vue @@ -29,6 +29,7 @@ import MkButton from '@/components/MkButton.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -38,7 +39,7 @@ const silencedHosts = ref<string>(''); const tab = ref('block'); async function init() { - const meta = await os.api('admin/meta'); + const meta = await misskeyApi('admin/meta'); blockedHosts.value = meta.blockedHosts.join('\n'); silencedHosts.value = meta.silencedHosts.join('\n'); } diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue index 6314d0ce4e..0cd1c63e07 100644 --- a/packages/frontend/src/pages/admin/invites.vue +++ b/packages/frontend/src/pages/admin/invites.vue @@ -59,6 +59,7 @@ import { computed, ref, shallowRef } from 'vue'; import XHeader from './_header_.vue'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSelect from '@/components/MkSelect.vue'; @@ -93,14 +94,14 @@ async function createWithOptions() { count: createCount.value, }; - const tickets = await os.api('admin/invite/create', options); + const tickets = await misskeyApi('admin/invite/create', options); os.alert({ type: 'success', title: i18n.ts.inviteCodeCreated, - text: tickets?.map(x => x.code).join('\n'), + text: tickets.map(x => x.code).join('\n'), }); - tickets?.forEach(ticket => pagingComponent.value?.prepend(ticket)); + tickets.forEach(ticket => pagingComponent.value?.prepend(ticket)); } function deleted(id: string) { diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 9539611f76..e8b0e306fc 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -75,6 +75,7 @@ import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -93,7 +94,7 @@ const tosUrl = ref<string | null>(null); const privacyPolicyUrl = ref<string | null>(null); async function init() { - const meta = await os.api('admin/meta'); + const meta = await misskeyApi('admin/meta'); enableRegistration.value = !meta.disableRegistration; emailRequiredForSignup.value = meta.emailRequiredForSignup; approvalRequiredForSignup.value = meta.approvalRequiredForSignup; diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue index e71e53c942..c412f5ba1d 100644 --- a/packages/frontend/src/pages/admin/object-storage.vue +++ b/packages/frontend/src/pages/admin/object-storage.vue @@ -90,6 +90,7 @@ import MkInput from '@/components/MkInput.vue'; import FormSuspense from '@/components/form/suspense.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -110,7 +111,7 @@ const objectStorageSetPublicRead = ref<boolean>(false); const objectStorageS3ForcePathStyle = ref<boolean>(true); async function init() { - const meta = await os.api('admin/meta'); + const meta = await misskeyApi('admin/meta'); useObjectStorage.value = meta.useObjectStorage; objectStorageBaseUrl.value = meta.objectStorageBaseUrl; objectStorageBucket.value = meta.objectStorageBucket; diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue index 6523676a18..690076d111 100644 --- a/packages/frontend/src/pages/admin/other-settings.vue +++ b/packages/frontend/src/pages/admin/other-settings.vue @@ -61,6 +61,7 @@ import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -74,7 +75,7 @@ const enableChartsForRemoteUser = ref<boolean>(false); const enableChartsForFederatedInstances = ref<boolean>(false); async function init() { - const meta = await os.api('admin/meta'); + const meta = await misskeyApi('admin/meta'); enableServerMachineStats.value = meta.enableServerMachineStats; enableAchievements.value = meta.enableAchievements; enableBotTrending.value = meta.enableBotTrending; diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue index 5e67370c2b..8acc5786b2 100644 --- a/packages/frontend/src/pages/admin/overview.active-users.vue +++ b/packages/frontend/src/pages/admin/overview.active-users.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import gradient from 'chartjs-plugin-gradient'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; @@ -52,7 +52,7 @@ async function renderChart() { })); }; - const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' }); const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue index 0de62fadea..694be9a590 100644 --- a/packages/frontend/src/pages/admin/overview.ap-requests.vue +++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import gradient from 'chartjs-plugin-gradient'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; import { defaultStore } from '@/store.js'; @@ -65,7 +65,7 @@ onMounted(async () => { })); }; - const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' }); const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const succColor = '#87e000'; diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue index 2fad222bda..5d74297106 100644 --- a/packages/frontend/src/pages/admin/overview.federation.vue +++ b/packages/frontend/src/pages/admin/overview.federation.vue @@ -49,6 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, ref } from 'vue'; import XPie, { type InstanceForPie } from './overview.pie.vue'; import * as os from '@/os.js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; import number from '@/filters/number.js'; import MkNumberDiff from '@/components/MkNumberDiff.vue'; import { i18n } from '@/i18n.js'; @@ -65,13 +66,13 @@ const fetching = ref(true); const { handler: externalTooltipHandler } = useChartTooltip(); onMounted(async () => { - const chart = await os.apiGet('charts/federation', { limit: 2, span: 'day' }); + const chart = await misskeyApiGet('charts/federation', { limit: 2, span: 'day' }); federationPubActive.value = chart.pubActive[0]; federationPubActiveDiff.value = chart.pubActive[0] - chart.pubActive[1]; federationSubActive.value = chart.subActive[0]; federationSubActiveDiff.value = chart.subActive[0] - chart.subActive[1]; - os.apiGet('federation/stats', { limit: 10 }).then(res => { + misskeyApiGet('federation/stats', { limit: 10 }).then(res => { topSubInstancesForPie.value = [ ...res.topSubInstances.map(x => ({ name: x.host, diff --git a/packages/frontend/src/pages/admin/overview.instances.vue b/packages/frontend/src/pages/admin/overview.instances.vue index de34f0c09b..8d731cbc90 100644 --- a/packages/frontend/src/pages/admin/overview.instances.vue +++ b/packages/frontend/src/pages/admin/overview.instances.vue @@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import * as Misskey from 'misskey-js'; -import * as os from '@/os.js'; import { useInterval } from '@/scripts/use-interval.js'; import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue'; import { defaultStore } from '@/store.js'; @@ -28,7 +28,7 @@ const instances = ref<Misskey.entities.FederationInstance[]>([]); const fetching = ref(true); const fetch = async () => { - const fetchedInstances = await os.api('federation/instances', { + const fetchedInstances = await misskeyApi('federation/instances', { sort: '+latestRequestReceivedAt', limit: 6, }); diff --git a/packages/frontend/src/pages/admin/overview.moderators.vue b/packages/frontend/src/pages/admin/overview.moderators.vue index 3034bdd57e..75b731996b 100644 --- a/packages/frontend/src/pages/admin/overview.moderators.vue +++ b/packages/frontend/src/pages/admin/overview.moderators.vue @@ -18,15 +18,15 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref } from 'vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import * as Misskey from 'misskey-js'; -import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; const moderators = ref<Misskey.entities.UserDetailed[] | null>(null); const fetching = ref(true); onMounted(async () => { - moderators.value = await os.api('admin/show-users', { + moderators.value = await misskeyApi('admin/show-users', { sort: '+lastActiveDate', state: 'adminOrModerator', limit: 30, diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue index adbfe3f9e2..ae66d2d612 100644 --- a/packages/frontend/src/pages/admin/overview.stats.vue +++ b/packages/frontend/src/pages/admin/overview.stats.vue @@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import * as os from '@/os.js'; +import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import MkNumberDiff from '@/components/MkNumberDiff.vue'; import MkNumber from '@/components/MkNumber.vue'; import { i18n } from '@/i18n.js'; @@ -78,17 +78,17 @@ const fetching = ref(true); onMounted(async () => { const [_stats, _onlineUsersCount] = await Promise.all([ - os.api('stats', {}), - os.apiGet('get-online-users-count').then(res => res.count), + misskeyApi('stats', {}), + misskeyApiGet('get-online-users-count').then(res => res.count), ]); stats.value = _stats; onlineUsersCount.value = _onlineUsersCount; - os.apiGet('charts/users', { limit: 2, span: 'day' }).then(chart => { + misskeyApiGet('charts/users', { limit: 2, span: 'day' }).then(chart => { usersComparedToThePrevDay.value = stats.value.originalUsersCount - chart.local.total[1]; }); - os.apiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => { + misskeyApiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => { notesComparedToThePrevDay.value = stats.value.originalNotesCount - chart.local.total[1]; }); diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue index 79579367c1..530ca0933e 100644 --- a/packages/frontend/src/pages/admin/overview.users.vue +++ b/packages/frontend/src/pages/admin/overview.users.vue @@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import * as Misskey from 'misskey-js'; -import * as os from '@/os.js'; import { useInterval } from '@/scripts/use-interval.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import { defaultStore } from '@/store.js'; @@ -28,7 +28,7 @@ const newUsers = ref<Misskey.entities.UserDetailed[] | null>(null); const fetching = ref(true); const fetch = async () => { - const _newUsers = await os.api('admin/show-users', { + const _newUsers = await misskeyApi('admin/show-users', { limit: 5, sort: '+createdAt', origin: 'local', diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue index 9f2920ee0c..39fea99109 100644 --- a/packages/frontend/src/pages/admin/overview.vue +++ b/packages/frontend/src/pages/admin/overview.vue @@ -79,6 +79,7 @@ import XModerators from './overview.moderators.vue'; import XHeatmap from './overview.heatmap.vue'; import type { InstanceForPie } from './overview.pie.vue'; import * as os from '@/os.js'; +import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -117,14 +118,14 @@ onMounted(async () => { magicGrid.listen(); */ - os.apiGet('charts/federation', { limit: 2, span: 'day' }).then(chart => { + misskeyApiGet('charts/federation', { limit: 2, span: 'day' }).then(chart => { federationPubActive.value = chart.pubActive[0]; federationPubActiveDiff.value = chart.pubActive[0] - chart.pubActive[1]; federationSubActive.value = chart.subActive[0]; federationSubActiveDiff.value = chart.subActive[0] - chart.subActive[1]; }); - os.apiGet('federation/stats', { limit: 10 }).then(res => { + misskeyApiGet('federation/stats', { limit: 10 }).then(res => { topSubInstancesForPie.value = [ ...res.topSubInstances.map(x => ({ name: x.host, @@ -149,18 +150,18 @@ onMounted(async () => { ]; }); - os.api('admin/server-info').then(serverInfoResponse => { + misskeyApi('admin/server-info').then(serverInfoResponse => { serverInfo.value = serverInfoResponse; }); - os.api('admin/show-users', { + misskeyApi('admin/show-users', { limit: 5, sort: '+createdAt', }).then(res => { newUsers.value = res; }); - os.api('federation/instances', { + misskeyApi('federation/instances', { sort: '+latestRequestReceivedAt', limit: 25, }).then(res => { diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue index 1425749bd4..860f4f4b2c 100644 --- a/packages/frontend/src/pages/admin/proxy-account.vue +++ b/packages/frontend/src/pages/admin/proxy-account.vue @@ -28,6 +28,7 @@ import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -36,10 +37,10 @@ const proxyAccount = ref<Misskey.entities.UserDetailed | null>(null); const proxyAccountId = ref<string | null>(null); async function init() { - const meta = await os.api('admin/meta'); + const meta = await misskeyApi('admin/meta'); proxyAccountId.value = meta.proxyAccountId; if (proxyAccountId.value) { - proxyAccount.value = await os.api('users/show', { userId: proxyAccountId.value }); + proxyAccount.value = await misskeyApi('users/show', { userId: proxyAccountId.value }); } } diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue index b829dd5738..dc45a211ae 100644 --- a/packages/frontend/src/pages/admin/queue.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.vue @@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue'; import XChart from './queue.chart.chart.vue'; import number from '@/filters/number.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import MkFolder from '@/components/MkFolder.vue'; @@ -105,7 +105,7 @@ const onStatsLog = (statsLog) => { onMounted(() => { if (props.domain === 'inbox' || props.domain === 'deliver') { - os.api(`admin/queue/${props.domain}-delayed`).then(result => { + misskeyApi(`admin/queue/${props.domain}-delayed`).then(result => { jobs.value = result; }); } diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue index 578c29ee6c..8eb0381212 100644 --- a/packages/frontend/src/pages/admin/relays.vue +++ b/packages/frontend/src/pages/admin/relays.vue @@ -29,6 +29,7 @@ import * as Misskey from 'misskey-js'; import XHeader from './_header_.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -41,7 +42,7 @@ async function addRelay() { placeholder: i18n.ts.inboxUrl, }); if (canceled) return; - os.api('admin/relays/add', { + misskeyApi('admin/relays/add', { inbox, }).then((relay: any) => { refresh(); @@ -54,7 +55,7 @@ async function addRelay() { } function remove(inbox: string) { - os.api('admin/relays/remove', { + misskeyApi('admin/relays/remove', { inbox, }).then(() => { refresh(); @@ -67,7 +68,7 @@ function remove(inbox: string) { } function refresh() { - os.api('admin/relays/list').then(relayList => { + misskeyApi('admin/relays/list').then(relayList => { relays.value = relayList; }); } diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue index 980c311156..9729d80eaa 100644 --- a/packages/frontend/src/pages/admin/roles.edit.vue +++ b/packages/frontend/src/pages/admin/roles.edit.vue @@ -28,11 +28,12 @@ import { v4 as uuid } from 'uuid'; import XHeader from './_header_.vue'; import XEditor from './roles.editor.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { useRouter } from '@/router.js'; import MkButton from '@/components/MkButton.vue'; import { rolesCache } from '@/cache.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -44,7 +45,7 @@ const role = ref<Misskey.entities.Role | null>(null); const data = ref<any>(null); if (props.id) { - role.value = await os.api('admin/roles/show', { + role.value = await misskeyApi('admin/roles/show', { roleId: props.id, }); diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index 92818cc3de..782d0736b4 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -67,14 +67,15 @@ import XHeader from './_header_.vue'; import XEditor from './roles.editor.vue'; import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { useRouter } from '@/router.js'; import MkButton from '@/components/MkButton.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkPagination from '@/components/MkPagination.vue'; import { infoImageUrl } from '@/instance.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -92,7 +93,7 @@ const usersPagination = { const expandedItems = ref([]); -const role = reactive(await os.api('admin/roles/show', { +const role = reactive(await misskeyApi('admin/roles/show', { roleId: props.id, })); diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 9cb48130d8..ca918faf8a 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -253,17 +253,18 @@ import MkRange from '@/components/MkRange.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkRolePreview from '@/components/MkRolePreview.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { instance } from '@/instance.js'; -import { useRouter } from '@/router.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import { ROLE_POLICIES } from '@/const.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); const baseRoleQ = ref(''); -const roles = await os.api('admin/roles/list'); +const roles = await misskeyApi('admin/roles/list'); const policies = reactive<Record<typeof ROLE_POLICIES[number], any>>({}); for (const ROLE_POLICY of ROLE_POLICIES) { diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index 8ed3e20af3..f8aa03ea45 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ph-shield ph-bold ph-lg"></i></template> <template #label>{{ i18n.ts.botProtection }}</template> <template v-if="enableHcaptcha" #suffix>hCaptcha</template> + <template v-else-if="enableMcaptcha" #suffix>mCaptcha</template> <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> <template v-else-if="enableTurnstile" #suffix>Turnstile</template> <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> @@ -37,6 +38,17 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> <template #label>Verifymail.io API Auth Key</template> </MkInput> + <MkSwitch v-model="enableTruemailApi" @update:modelValue="save"> + <template #label>Use TrueMail API</template> + </MkSwitch> + <MkInput v-model="truemailInstance" @update:modelValue="save"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>TrueMail API Instance</template> + </MkInput> + <MkInput v-model="truemailAuthKey" @update:modelValue="save"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>TrueMail API Auth Key</template> + </MkInput> </div> </MkFolder> @@ -94,24 +106,30 @@ import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; const summalyProxy = ref<string>(''); const enableHcaptcha = ref<boolean>(false); +const enableMcaptcha = ref<boolean>(false); const enableRecaptcha = ref<boolean>(false); const enableTurnstile = ref<boolean>(false); const enableIpLogging = ref<boolean>(false); const enableActiveEmailValidation = ref<boolean>(false); const enableVerifymailApi = ref<boolean>(false); const verifymailAuthKey = ref<string | null>(null); +const enableTruemailApi = ref<boolean>(false); +const truemailInstance = ref<string | null>(null); +const truemailAuthKey = ref<string | null>(null); const bannedEmailDomains = ref<string>(''); async function init() { - const meta = await os.api('admin/meta'); + const meta = await misskeyApi('admin/meta'); summalyProxy.value = meta.summalyProxy; enableHcaptcha.value = meta.enableHcaptcha; + enableMcaptcha.value = meta.enableMcaptcha; enableRecaptcha.value = meta.enableRecaptcha; enableTurnstile.value = meta.enableTurnstile; enableIpLogging.value = meta.enableIpLogging; @@ -128,6 +146,9 @@ function save() { enableActiveEmailValidation: enableActiveEmailValidation.value, enableVerifymailApi: enableVerifymailApi.value, verifymailAuthKey: verifymailAuthKey.value, + enableTruemailApi: enableTruemailApi.value, + truemailInstance: truemailInstance.value, + truemailAuthKey: truemailAuthKey.value, bannedEmailDomains: bannedEmailDomains.value.split('\n'), }).then(() => { fetchInstance(); diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 649f22e644..d08bfac74a 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -158,6 +158,7 @@ import FormSection from '@/components/form/section.vue'; import FormSplit from '@/components/form/split.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -184,7 +185,7 @@ const perUserListTimelineCacheMax = ref<number>(0); const notesPerOneAd = ref<number>(0); async function init(): Promise<void> { - const meta = await os.api('admin/meta'); + const meta = await misskeyApi('admin/meta'); name.value = meta.name; shortName.value = meta.shortName; description.value = meta.description; diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index 705115abb0..5c334bc06b 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -45,6 +45,7 @@ import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { $i, updateAccount } from '@/account.js'; @@ -84,7 +85,7 @@ async function read(announcement) { a.isRead = true; return a; }); - os.api('i/read-announcement', { announcementId: announcement.id }); + misskeyApi('i/read-announcement', { announcementId: announcement.id }); updateAccount({ unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== announcement.id), }); diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index 9abf0b9776..febc77136b 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -29,9 +29,10 @@ import * as Misskey from 'misskey-js'; import MkTimeline from '@/components/MkTimeline.vue'; import { scroll } from '@/scripts/scroll.js'; import * as os from '@/os.js'; -import { useRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -73,7 +74,7 @@ function focus() { } watch(() => props.antennaId, async () => { - antenna.value = await os.api('antennas/show', { + antenna.value = await misskeyApi('antennas/show', { antennaId: props.antennaId, }); }, { immediate: true }); diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue index dcdb5b8fe3..b068c6f30c 100644 --- a/packages/frontend/src/pages/api-console.vue +++ b/packages/frontend/src/pages/api-console.vue @@ -41,7 +41,7 @@ import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkSwitch from '@/components/MkSwitch.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; const body = ref('{}'); @@ -51,14 +51,14 @@ const sending = ref(false); const res = ref(''); const withCredential = ref(true); -os.api('endpoints').then(endpointResponse => { +misskeyApi('endpoints').then(endpointResponse => { endpoints.value = endpointResponse; }); function send() { sending.value = true; const requestBody = JSON5.parse(body.value); - os.api(endpoint.value as keyof Endpoints, requestBody, requestBody.i || (withCredential.value ? undefined : null)).then(resp => { + misskeyApi(endpoint.value as keyof Endpoints, requestBody, requestBody.i || (withCredential.value ? undefined : null)).then(resp => { sending.value = false; res.value = JSON5.stringify(resp, null, 2); }, err => { @@ -68,7 +68,7 @@ function send() { } function onEndpointChange() { - os.api('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null).then(resp => { + misskeyApi('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null).then(resp => { const endpointBody = {}; for (const p of resp.params) { endpointBody[p.name] = diff --git a/packages/frontend/src/pages/auth.form.vue b/packages/frontend/src/pages/auth.form.vue index 8a17e5895d..39a7924f94 100644 --- a/packages/frontend/src/pages/auth.form.vue +++ b/packages/frontend/src/pages/auth.form.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -44,7 +44,7 @@ const name = computed(() => { }); function cancel() { - os.api('auth/deny', { + misskeyApi('auth/deny', { token: props.session.token, }).then(() => { emit('denied'); @@ -52,7 +52,7 @@ function cancel() { } function accept() { - os.api('auth/accept', { + misskeyApi('auth/accept', { token: props.session.token, }).then(() => { emit('accepted'); diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue index d97e89842d..9faa56e148 100644 --- a/packages/frontend/src/pages/auth.vue +++ b/packages/frontend/src/pages/auth.vue @@ -46,7 +46,7 @@ import { onMounted, ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import XForm from './auth.form.vue'; import MkSignin from '@/components/MkSignin.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i, login } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; @@ -96,13 +96,13 @@ onMounted(async () => { if (!$i) return; try { - session.value = await os.api('auth/session/show', { + session.value = await misskeyApi('auth/session/show', { token: props.token, }); // 既に連携していた場合 if (session.value.app.isAuthorized) { - await os.api('auth/accept', { + await misskeyApi('auth/accept', { token: session.value.token, }); accepted(); diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue index 30b100a7fb..3092bf86e1 100644 --- a/packages/frontend/src/pages/avatar-decorations.vue +++ b/packages/frontend/src/pages/avatar-decorations.vue @@ -40,6 +40,7 @@ import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkFolder from '@/components/MkFolder.vue'; @@ -63,7 +64,7 @@ function del(avatarDecoration) { }).then(({ canceled }) => { if (canceled) return; avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration); - os.api('admin/avatar-decorations/delete', avatarDecoration); + misskeyApi('admin/avatar-decorations/delete', avatarDecoration); }); } @@ -77,7 +78,7 @@ async function save(avatarDecoration) { } function load() { - os.api('admin/avatar-decorations/list').then(_avatarDecorations => { + misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => { avatarDecorations.value = _avatarDecorations; }); } diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index 912d02c7fc..e728622441 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -76,12 +76,13 @@ import MkInput from '@/components/MkInput.vue'; import MkColorInput from '@/components/MkColorInput.vue'; import { selectFile } from '@/scripts/select-file.js'; import * as os from '@/os.js'; -import { useRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkTextarea from '@/components/MkTextarea.vue'; +import { useRouter } from '@/global/router/supplier.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -105,7 +106,7 @@ watch(() => bannerId.value, async () => { if (bannerId.value == null) { bannerUrl.value = null; } else { - bannerUrl.value = (await os.api('drive/files/show', { + bannerUrl.value = (await misskeyApi('drive/files/show', { fileId: bannerId.value, })).url; } @@ -114,7 +115,7 @@ watch(() => bannerId.value, async () => { async function fetchChannel() { if (props.channelId == null) return; - channel.value = await os.api('channels/show', { + channel.value = await misskeyApi('channels/show', { channelId: props.channelId, }); @@ -179,7 +180,7 @@ async function archive() { if (canceled) return; - os.api('channels/update', { + misskeyApi('channels/update', { channelId: props.channelId, isArchived: true, }).then(() => { diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index b0873ea336..38658798e8 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -74,7 +74,7 @@ import MkPostForm from '@/components/MkPostForm.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import XChannelFollowButton from '@/components/MkChannelFollowButton.vue'; import * as os from '@/os.js'; -import { useRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i, iAmModerator } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -91,6 +91,7 @@ import { PageHeaderItem } from '@/types/page-header.js'; import { isSupportShare } from '@/scripts/navigator.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { miLocalStorage } from '@/local-storage.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -113,7 +114,7 @@ const featuredPagination = computed(() => ({ })); watch(() => props.channelId, async () => { - channel.value = await os.api('channels/show', { + channel.value = await misskeyApi('channels/show', { channelId: props.channelId, }); favorited.value = channel.value.isFavorited ?? false; diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue index 63d1e454a2..58ba120e3a 100644 --- a/packages/frontend/src/pages/channels.vue +++ b/packages/frontend/src/pages/channels.vue @@ -58,9 +58,9 @@ import MkInput from '@/components/MkInput.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkButton from '@/components/MkButton.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; -import { useRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 9b5f0224cc..d214e2882a 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -32,6 +32,7 @@ import MkNotes from '@/components/MkNotes.vue'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { url } from '@/config.js'; import MkButton from '@/components/MkButton.vue'; @@ -56,7 +57,7 @@ const pagination = { const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId)); watch(() => props.clipId, async () => { - clip.value = await os.api('clips/show', { + clip.value = await misskeyApi('clips/show', { clipId: props.clipId, }); favorited.value = clip.value.isFavorited; diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index bc2a268f34..a597497c95 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -82,6 +82,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import FormSplit from '@/components/form/split.vue'; import { selectFile } from '@/scripts/select-file.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -187,7 +188,7 @@ const menu = (ev: MouseEvent) => { icon: 'ph-download ph-bold ph-lg', text: i18n.ts.export, action: async () => { - os.api('export-custom-emojis', { + misskeyApi('export-custom-emojis', { }) .then(() => { os.alert({ @@ -206,7 +207,7 @@ const menu = (ev: MouseEvent) => { text: i18n.ts.import, action: async () => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('admin/emoji/import-zip', { + misskeyApi('admin/emoji/import-zip', { fileId: file.id, }) .then(() => { diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index 69309b37f3..3133e55075 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -79,7 +79,8 @@ import bytes from '@/filters/bytes.js'; import { infoImageUrl } from '@/instance.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { useRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -94,7 +95,7 @@ const isImage = computed(() => file.value?.type.startsWith('image/')); async function fetch() { fetching.value = true; - file.value = await os.api('drive/files/show', { + file.value = await misskeyApi('drive/files/show', { fileId: props.fileId, }).catch((err) => { console.error(err); diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue new file mode 100644 index 0000000000..0ddee55f5f --- /dev/null +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -0,0 +1,825 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer> + <template #header><MkPageHeader/></template> + <MkSpacer :contentMax="800"> + <div v-show="!gameStarted" :class="$style.root"> + <div style="text-align: center;" class="_gaps"> + <div :class="$style.frame"> + <div :class="$style.frameInner"> + <img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/> + </div> + </div> + <div :class="$style.frame"> + <div :class="$style.frameInner"> + <div class="_gaps" style="padding: 16px;"> + <MkSelect v-model="gameMode"> + <option value="normal">NORMAL</option> + <option value="square">SQUARE</option> + </MkSelect> + <MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton> + </div> + </div> + </div> + </div> + </div> + <div v-show="gameStarted" class="_gaps_s" :class="$style.root"> + <div style="display: flex;"> + <div :class="$style.frame" style="flex: 1; margin-right: 10px;"> + <div :class="$style.frameInner"> + <b>BUBBLE GAME</b> + <div>- {{ gameMode }} -</div> + </div> + </div> + <div :class="[$style.frame, $style.stock]" style="margin-left: auto;"> + <div :class="$style.frameInner" style="text-align: center;"> + NEXT >>> + <TransitionGroup + :enterActiveClass="$style.transition_stock_enterActive" + :leaveActiveClass="$style.transition_stock_leaveActive" + :enterFromClass="$style.transition_stock_enterFrom" + :leaveToClass="$style.transition_stock_leaveTo" + :moveClass="$style.transition_stock_move" + > + <div v-for="x in stock" :key="x.id" style="display: inline-block;"> + <img :src="game.getTextureImageUrl(x.mono)" style="width: 32px;"/> + </div> + </TransitionGroup> + </div> + </div> + </div> + <div :class="$style.main" @contextmenu.stop.prevent> + <div ref="containerEl" :class="[$style.container, { [$style.gameOver]: gameOver }]" @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove"> + <img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/> + <img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/> + <canvas ref="canvasEl" :class="$style.canvas"/> + <Transition + :enterActiveClass="$style.transition_combo_enterActive" + :leaveActiveClass="$style.transition_combo_leaveActive" + :enterFromClass="$style.transition_combo_enterFrom" + :leaveToClass="$style.transition_combo_leaveTo" + :moveClass="$style.transition_combo_move" + > + <div v-show="combo > 1" :class="$style.combo" :style="{ fontSize: `${100 + ((comboPrev - 2) * 15)}%` }">{{ comboPrev }} Chain!</div> + </Transition> + <img v-if="currentPick" src="/client-assets/drop-and-fusion/dropper.png" :class="$style.dropper" :style="{ left: dropperX + 'px' }"/> + <Transition + :enterActiveClass="$style.transition_picked_enterActive" + :leaveActiveClass="$style.transition_picked_leaveActive" + :enterFromClass="$style.transition_picked_enterFrom" + :leaveToClass="$style.transition_picked_leaveTo" + :moveClass="$style.transition_picked_move" + mode="out-in" + > + <img v-if="currentPick" :key="currentPick.id" :src="game.getTextureImageUrl(currentPick.mono)" :class="$style.currentMono" :style="{ top: -(currentPick?.mono.size / 2) + 'px', left: (dropperX - (currentPick?.mono.size / 2)) + 'px', width: `${currentPick?.mono.size}px` }"/> + </Transition> + <template v-if="dropReady && currentPick"> + <img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow" :style="{ top: (currentPick.mono.size / 2) + 10 + 'px', left: (dropperX - 10) + 'px', width: `20px` }"/> + <div :class="$style.dropGuide" :style="{ left: (dropperX - 2) + 'px' }"/> + </template> + <div v-if="gameOver" :class="$style.gameOverLabel"> + <div class="_gaps_s"> + <img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/> + <div>SCORE: <MkNumber :value="score"/></div> + <div>MAX CHAIN: <MkNumber :value="maxCombo"/></div> + <div class="_buttonsCenter"> + <MkButton primary rounded @click="restart">Restart</MkButton> + <MkButton primary rounded @click="share">Share</MkButton> + </div> + </div> + </div> + </div> + </div> + <div style="display: flex;"> + <div :class="$style.frame" style="flex: 1; margin-right: 10px;"> + <div :class="$style.frameInner"> + <div>SCORE: <b><MkNumber :value="score"/></b> (MAX CHAIN: <b><MkNumber :value="maxCombo"/></b>)</div> + <div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/></b><b v-else>-</b></div> + </div> + </div> + <div :class="[$style.frame]" style="margin-left: auto;"> + <div :class="$style.frameInner" style="text-align: center;"> + <div @click="showConfig = !showConfig"><i class="ti ti-settings"></i></div> + </div> + </div> + </div> + <div v-if="showConfig" :class="$style.frame"> + <div :class="$style.frameInner"> + <MkRange v-model="bgmVolume" :min="0" :max="1" :step="0.0025" :textConverter="(v) => `${Math.floor(v * 100)}%`" :continuousUpdate="true"> + <template #label>BGM {{ i18n.ts.volume }}</template> + </MkRange> + </div> + </div> + <div v-if="showConfig" :class="$style.frame"> + <div :class="$style.frameInner"> + <div>Credit</div> + <div>BGM: @ys@misskey.design</div> + </div> + </div> + <div :class="$style.frame"> + <div :class="$style.frameInner"> + <MkButton @click="restart">Restart</MkButton> + </div> + </div> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { onDeactivated, ref, shallowRef, watch } from 'vue'; +import * as Misskey from 'misskey-js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkRippleEffect from '@/components/MkRippleEffect.vue'; +import * as os from '@/os.js'; +import MkNumber from '@/components/MkNumber.vue'; +import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; +import MkButton from '@/components/MkButton.vue'; +import { claimAchievement } from '@/scripts/achievements.js'; +import { defaultStore } from '@/store.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import { useInterval } from '@/scripts/use-interval.js'; +import MkSelect from '@/components/MkSelect.vue'; +import { apiUrl } from '@/config.js'; +import { $i } from '@/account.js'; +import { DropAndFusionGame, Mono } from '@/scripts/drop-and-fusion-engine.js'; +import * as sound from '@/scripts/sound.js'; +import MkRange from '@/components/MkRange.vue'; + +const containerEl = shallowRef<HTMLElement>(); +const canvasEl = shallowRef<HTMLCanvasElement>(); +const dropperX = ref(0); + +const NORMAL_BASE_SIZE = 30; +const NORAML_MONOS: Mono[] = [{ + id: '9377076d-c980-4d83-bdaf-175bc58275b7', + level: 10, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 512, + dropCandidate: false, + sfxPitch: 0.25, + img: '/client-assets/drop-and-fusion/exploding_head.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'be9f38d2-b267-4b1a-b420-904e22e80568', + level: 9, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 256, + dropCandidate: false, + sfxPitch: 0.5, + img: '/client-assets/drop-and-fusion/face_with_symbols_on_mouth.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'beb30459-b064-4888-926b-f572e4e72e0c', + level: 8, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 128, + dropCandidate: false, + sfxPitch: 0.75, + img: '/client-assets/drop-and-fusion/cold_face.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'feab6426-d9d8-49ae-849c-048cdbb6cdf0', + level: 7, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 64, + dropCandidate: false, + sfxPitch: 1, + img: '/client-assets/drop-and-fusion/zany_face.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'd6d8fed6-6d18-4726-81a1-6cf2c974df8a', + level: 6, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 32, + dropCandidate: false, + sfxPitch: 1.5, + img: '/client-assets/drop-and-fusion/pleading_face.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '249c728e-230f-4332-bbbf-281c271c75b2', + level: 5, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 16, + dropCandidate: true, + sfxPitch: 2, + img: '/client-assets/drop-and-fusion/face_with_open_mouth.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '23d67613-d484-4a93-b71e-3e81b19d6186', + level: 4, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 8, + dropCandidate: true, + sfxPitch: 2.5, + img: '/client-assets/drop-and-fusion/smiling_face_with_sunglasses.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '3cbd0add-ad7d-4685-bad0-29f6dddc0b99', + level: 3, + size: NORMAL_BASE_SIZE * 1.25 * 1.25, + shape: 'circle', + score: 4, + dropCandidate: true, + sfxPitch: 3, + img: '/client-assets/drop-and-fusion/grinning_squinting_face.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '8f86d4f4-ee02-41bf-ad38-1ce0ae457fb5', + level: 2, + size: NORMAL_BASE_SIZE * 1.25, + shape: 'circle', + score: 2, + dropCandidate: true, + sfxPitch: 3.5, + img: '/client-assets/drop-and-fusion/smiling_face_with_hearts.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '64ec4add-ce39-42b4-96cb-33908f3f118d', + level: 1, + size: NORMAL_BASE_SIZE, + shape: 'circle', + score: 1, + dropCandidate: true, + sfxPitch: 4, + img: '/client-assets/drop-and-fusion/heart_suit.png', + imgSize: 256, + spriteScale: 1.12, +}]; + +const SQUARE_BASE_SIZE = 28; +const SQUARE_MONOS: Mono[] = [{ + id: 'f75fd0ba-d3d4-40a4-9712-b470e45b0525', + level: 10, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 512, + dropCandidate: false, + sfxPitch: 0.25, + img: '/client-assets/drop-and-fusion/keycap_10.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '7b70f4af-1c01-45fd-af72-61b1f01e03d1', + level: 9, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 256, + dropCandidate: false, + sfxPitch: 0.5, + img: '/client-assets/drop-and-fusion/keycap_9.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '41607ef3-b6d6-4829-95b6-3737bf8bb956', + level: 8, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 128, + dropCandidate: false, + sfxPitch: 0.75, + img: '/client-assets/drop-and-fusion/keycap_8.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '8a8310d2-0374-460f-bb50-ca9cd3ee3416', + level: 7, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 64, + dropCandidate: false, + sfxPitch: 1, + img: '/client-assets/drop-and-fusion/keycap_7.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '1092e069-fe1a-450b-be97-b5d477ec398c', + level: 6, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 32, + dropCandidate: false, + sfxPitch: 1.5, + img: '/client-assets/drop-and-fusion/keycap_6.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '2294734d-7bb8-4781-bb7b-ef3820abf3d0', + level: 5, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 16, + dropCandidate: true, + sfxPitch: 2, + img: '/client-assets/drop-and-fusion/keycap_5.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'ea8a61af-e350-45f7-ba6a-366fcd65692a', + level: 4, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 8, + dropCandidate: true, + sfxPitch: 2.5, + img: '/client-assets/drop-and-fusion/keycap_4.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'd0c74815-fc1c-4fbe-9953-c92e4b20f919', + level: 3, + size: SQUARE_BASE_SIZE * 1.25 * 1.25, + shape: 'rectangle', + score: 4, + dropCandidate: true, + sfxPitch: 3, + img: '/client-assets/drop-and-fusion/keycap_3.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'd8fbd70e-611d-402d-87da-1a7fd8cd2c8d', + level: 2, + size: SQUARE_BASE_SIZE * 1.25, + shape: 'rectangle', + score: 2, + dropCandidate: true, + sfxPitch: 3.5, + img: '/client-assets/drop-and-fusion/keycap_2.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '35e476ee-44bd-4711-ad42-87be245d3efd', + level: 1, + size: SQUARE_BASE_SIZE, + shape: 'rectangle', + score: 1, + dropCandidate: true, + sfxPitch: 4, + img: '/client-assets/drop-and-fusion/keycap_1.png', + imgSize: 256, + spriteScale: 1.12, +}]; + +const GAME_WIDTH = 450; +const GAME_HEIGHT = 600; + +let viewScaleX = 1; +let viewScaleY = 1; +const currentPick = shallowRef<{ id: string; mono: Mono } | null>(null); +const stock = shallowRef<{ id: string; mono: Mono }[]>([]); +const score = ref(0); +const combo = ref(0); +const comboPrev = ref(0); +const maxCombo = ref(0); +const dropReady = ref(true); +const gameMode = ref<'normal' | 'square'>('normal'); +const gameOver = ref(false); +const gameStarted = ref(false); +const highScore = ref<number | null>(null); +const showConfig = ref(false); +const bgmVolume = ref(0.1); + +let game: DropAndFusionGame; +let containerElRect: DOMRect | null = null; + +function onClick(ev: MouseEvent) { + if (!containerElRect) return; + const x = (ev.clientX - containerElRect.left) / viewScaleX; + game.drop(x); +} + +function onTouchend(ev: TouchEvent) { + if (!containerElRect) return; + const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScaleX; + game.drop(x); +} + +function onMousemove(ev: MouseEvent) { + if (!containerElRect) return; + const x = (ev.clientX - containerElRect.left); + moveDropper(containerElRect, x); +} + +function onTouchmove(ev: TouchEvent) { + if (!containerElRect) return; + const x = (ev.touches[0].clientX - containerElRect.left); + moveDropper(containerElRect, x); +} + +function moveDropper(rect: DOMRect, x: number) { + dropperX.value = Math.min(rect.width * ((GAME_WIDTH - game.PLAYAREA_MARGIN) / GAME_WIDTH), Math.max(rect.width * (game.PLAYAREA_MARGIN / GAME_WIDTH), x)); +} + +function restart() { + game.dispose(); + gameOver.value = false; + currentPick.value = null; + dropReady.value = true; + stock.value = []; + score.value = 0; + combo.value = 0; + comboPrev.value = 0; + gameStarted.value = false; +} + +function attachGameEvents() { + game.addListener('changeScore', value => { + score.value = value; + }); + + game.addListener('changeCombo', value => { + if (value === 0) { + comboPrev.value = combo.value; + } else { + comboPrev.value = value; + } + maxCombo.value = Math.max(maxCombo.value, value); + combo.value = value; + }); + + game.addListener('changeStock', value => { + currentPick.value = JSON.parse(JSON.stringify(value[0])); + stock.value = JSON.parse(JSON.stringify(value.slice(1))); + }); + + game.addListener('dropped', () => { + dropReady.value = false; + window.setTimeout(() => { + if (!gameOver.value) { + dropReady.value = true; + } + }, game.DROP_INTERVAL); + }); + + game.addListener('fusioned', (x, y, scoreDelta) => { + if (!canvasEl.value) return; + + const rect = canvasEl.value.getBoundingClientRect(); + const domX = rect.left + (x * viewScaleX); + const domY = rect.top + (y * viewScaleY); + os.popup(MkRippleEffect, { x: domX, y: domY }, {}, 'end'); + os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta }, {}, 'end'); + }); + + game.addListener('monoAdded', (mono) => { + // 実績関連 + if (mono.level === 10) { + claimAchievement('bubbleGameExplodingHead'); + + const monos = game.getActiveMonos(); + if (monos.filter(x => x.level === 10).length >= 2) { + claimAchievement('bubbleGameDoubleExplodingHead'); + } + } + }); + + game.addListener('gameOver', () => { + currentPick.value = null; + dropReady.value = false; + gameOver.value = true; + + if (score.value > (highScore.value ?? 0)) { + highScore.value = score.value; + + misskeyApi('i/registry/set', { + scope: ['dropAndFusionGame'], + key: 'highScore:' + gameMode.value, + value: highScore.value, + }); + } + }); +} + +let bgmNodes: ReturnType<typeof sound.createSourceNode> = null; + +async function start() { + try { + highScore.value = await misskeyApi('i/registry/get', { + scope: ['dropAndFusionGame'], + key: 'highScore:' + gameMode.value, + }); + } catch (err) { + highScore.value = null; + } + + game = new DropAndFusionGame({ + width: GAME_WIDTH, + height: GAME_HEIGHT, + canvas: canvasEl.value!, + ...( + gameMode.value === 'normal' ? { + monoDefinitions: NORAML_MONOS, + } : { + monoDefinitions: SQUARE_MONOS, + } + ), + }); + attachGameEvents(); + os.promiseDialog(game.load(), async () => { + game.start(); + gameStarted.value = true; + + if (bgmNodes) { + bgmNodes.soundSource.stop(); + bgmNodes = null; + } + const bgmBuffer = await sound.loadAudio('/client-assets/drop-and-fusion/bgm_1.mp3'); + if (!bgmBuffer) return; + bgmNodes = sound.createSourceNode(bgmBuffer, bgmVolume.value); + if (!bgmNodes) return; + bgmNodes.soundSource.loop = true; + bgmNodes.soundSource.start(); + }); +} + +watch(bgmVolume, (value) => { + if (bgmNodes) { + bgmNodes.gainNode.gain.value = value; + } +}); + +function getGameImageDriveFile() { + return new Promise<Misskey.entities.DriveFile | null>(res => { + const dcanvas = document.createElement('canvas'); + dcanvas.width = GAME_WIDTH; + dcanvas.height = GAME_HEIGHT; + const ctx = dcanvas.getContext('2d'); + if (!ctx || !canvasEl.value) return res(null); + const dimage = new Image(); + dimage.src = '/client-assets/drop-and-fusion/frame-light.svg'; + dimage.addEventListener('load', () => { + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); + ctx.drawImage(dimage, 0, 0, GAME_WIDTH, GAME_HEIGHT); + ctx.drawImage(canvasEl.value!, 0, 0, GAME_WIDTH, GAME_HEIGHT); + + dcanvas.toBlob(blob => { + if (!blob) return res(null); + if ($i == null) return res(null); + const formData = new FormData(); + formData.append('file', blob); + formData.append('name', `bubble-game-${Date.now()}.png`); + formData.append('isSensitive', 'false'); + formData.append('comment', 'null'); + formData.append('i', $i.token); + if (defaultStore.state.uploadFolder) { + formData.append('folderId', defaultStore.state.uploadFolder); + } + + window.fetch(apiUrl + '/drive/files/create', { + method: 'POST', + body: formData, + }) + .then(response => response.json()) + .then(f => { + res(f); + }); + }, 'image/png'); + + dcanvas.remove(); + }); + }); +} + +async function share() { + const uploading = getGameImageDriveFile(); + os.promiseDialog(uploading); + const file = await uploading; + if (!file) return; + os.post({ + initialText: `#BubbleGame +MODE: ${gameMode.value} +SCORE: ${score.value} (MAX CHAIN: ${maxCombo.value})})`, + initialFiles: [file], + }); +} + +useInterval(() => { + if (!canvasEl.value) return; + const actualCanvasWidth = canvasEl.value.getBoundingClientRect().width; + const actualCanvasHeight = canvasEl.value.getBoundingClientRect().height; + viewScaleX = actualCanvasWidth / GAME_WIDTH; + viewScaleY = actualCanvasHeight / GAME_HEIGHT; + containerElRect = containerEl.value?.getBoundingClientRect() ?? null; +}, 1000, { immediate: false, afterMounted: true }); + +onDeactivated(() => { + game.dispose(); +}); + +definePageMetadata({ + title: i18n.ts.bubbleGame, + icon: 'ti ti-apple', +}); +</script> + +<style lang="scss" module> +.transition_stock_move, +.transition_stock_enterActive, +.transition_stock_leaveActive { + transition: opacity 0.4s cubic-bezier(0,.5,.5,1), transform 0.4s cubic-bezier(0,.5,.5,1) !important; +} +.transition_stock_enterFrom, +.transition_stock_leaveTo { + opacity: 0; + transform: scale(0.7); +} +.transition_stock_leaveActive { + position: absolute; +} + +.transition_picked_move, +.transition_picked_enterActive { + transition: opacity 0.5s cubic-bezier(0,.5,.5,1), transform 0.5s cubic-bezier(0,.5,.5,1) !important; +} +.transition_picked_leaveActive { + transition: all 0s !important; +} +.transition_picked_enterFrom, +.transition_picked_leaveTo { + opacity: 0; + transform: translateY(-50px); +} +.transition_picked_leaveActive { + position: absolute; +} + +.transition_combo_move, +.transition_combo_enterActive { + transition: all 0s !important; +} +.transition_combo_leaveActive { + transition: opacity 0.4s cubic-bezier(0,.5,.5,1), transform 0.4s cubic-bezier(0,.5,.5,1) !important; +} +.transition_combo_enterFrom, +.transition_combo_leaveTo { + opacity: 0; + transform: scale(0.7); +} +.transition_combo_leaveActive { + position: absolute; +} + +.root { + margin: 0 auto; + max-width: 600px; + user-select: none; + + * { + user-select: none; + } +} + +.frame { + padding: 7px; + background: #8C4F26; + box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c; + border-radius: 10px; +} +.frameInner { + padding: 4px 8px; + background: #F1E8DC; + box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410; + border-radius: 6px; + color: #693410; +} + +.main { + position: relative; +} + +.mainFrameImg { + position: absolute; + top: 0; + left: 0; + width: 100%; + // なんかiOSでちらつく + //filter: drop-shadow(0 6px 16px #0007); + pointer-events: none; + user-select: none; +} + +.canvas { + position: relative; + display: block; + z-index: 1; + margin-top: -50px; + width: 100% !important; + height: auto !important; + pointer-events: none; + user-select: none; +} + +.container { + position: relative; +} + +.stock { + pointer-events: none; + user-select: none; +} + +.combo { + position: absolute; + z-index: 3; + top: 50%; + width: 100%; + text-align: center; + font-weight: bold; + font-style: oblique; + color: #fff; + -webkit-text-stroke: 1px rgb(255, 145, 0); + text-shadow: 0 0 6px #0005; + pointer-events: none; + user-select: none; +} + +.currentMono { + position: absolute; + margin-top: 80px; + z-index: 2; + filter: drop-shadow(0 6px 16px #0007); + pointer-events: none; + user-select: none; +} + +.dropper { + position: absolute; + top: 0; + width: 70px; + margin-top: -10px; + margin-left: -30px; + z-index: 2; + filter: drop-shadow(0 6px 16px #0007); + pointer-events: none; + user-select: none; +} + +.currentMonoArrow { + position: absolute; + margin-top: 100px; + z-index: 3; + animation: currentMonoArrow 2s ease infinite; + pointer-events: none; + user-select: none; +} + +.dropGuide { + position: absolute; + top: 120px; + z-index: 3; + width: 3px; + height: calc(100% - 120px); + background: #f002; + pointer-events: none; + user-select: none; +} + +.gameOverLabel { + position: absolute; + z-index: 10; + top: 50%; + width: 100%; + padding: 16px; + box-sizing: border-box; + background: #0007; + color: #fff; + text-align: center; + font-weight: bold; +} + +.gameOver { + .canvas { + filter: grayscale(1); + } +} + +@keyframes currentMonoArrow { + 0% { transform: translateY(0); } + 25% { transform: translateY(-8px); } + 50% { transform: translateY(0); } + 75% { transform: translateY(-8px); } + 100% { transform: translateY(0); } +} +</style> diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 82cfa92f6a..3cb7c6f40b 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -39,7 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkInput v-model="aliases" autocapitalize="off"> <template #label>{{ i18n.ts.tags }}</template> - <template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template> + <template #caption> + {{ i18n.ts.theKeywordWhenSearchingForCustomEmoji }}<br/> + {{ i18n.ts.setMultipleBySeparatingWithSpace }} + </template> </MkInput> <MkInput v-model="license"> <template #label>{{ i18n.ts.license }}</template> @@ -82,6 +85,7 @@ import MkInput from '@/components/MkInput.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { customEmojiCategories } from '@/custom-emojis.js'; import MkSwitch from '@/components/MkSwitch.vue'; @@ -104,7 +108,7 @@ const rolesThatCanBeUsedThisEmojiAsReaction = ref<Misskey.entities.Role[]>([]); const file = ref<Misskey.entities.DriveFile>(); watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => { - rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); + rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); }, { immediate: true }); const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null); @@ -123,7 +127,7 @@ async function changeImage(ev) { } async function addRole() { - const roles = await os.api('admin/roles/list'); + const roles = await misskeyApi('admin/roles/list'); const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id); const { canceled, result: role } = await os.select({ @@ -185,7 +189,7 @@ async function del() { }); if (canceled) return; - os.api('admin/emoji/delete', { + misskeyApi('admin/emoji/delete', { id: props.emoji.id, }).then(() => { emit('done', { diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue index d94fe96fa2..6d97784947 100644 --- a/packages/frontend/src/pages/emojis.emoji.vue +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; import * as os from '@/os.js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { i18n } from '@/i18n.js'; @@ -43,7 +44,7 @@ function menu(ev) { text: i18n.ts.info, icon: 'ph-info ph-bold ph-lg', action: () => { - os.apiGet('emoji', { name: props.emoji.name }).then(res => { + misskeyApiGet('emoji', { name: props.emoji.name }).then(res => { os.alert({ type: 'info', text: `Name: ${res.name}\nAliases: ${res.aliases.join(' ')}\nCategory: ${res.category}\nisSensitive: ${res.isSensitive}\nlocalOnly: ${res.localOnly}\nLicense: ${res.license}\nURL: ${res.url}`, diff --git a/packages/frontend/src/pages/explore.roles.vue b/packages/frontend/src/pages/explore.roles.vue index d30e107e97..f06bd0840c 100644 --- a/packages/frontend/src/pages/explore.roles.vue +++ b/packages/frontend/src/pages/explore.roles.vue @@ -15,11 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkRolePreview from '@/components/MkRolePreview.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const roles = ref<Misskey.entities.Role[] | null>(null); -os.api('roles/list').then(res => { +misskeyApi('roles/list').then(res => { roles.value = res.filter(x => x.target === 'manual').sort((a, b) => b.displayOrder - a.displayOrder); }); </script> diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue index fbca2b8ede..7038b313f3 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -68,7 +68,7 @@ import * as Misskey from 'misskey-js'; import MkUserList from '@/components/MkUserList.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkTab from '@/components/MkTab.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -123,14 +123,14 @@ const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, sort: '+createdAt', } }; -os.api('hashtags/list', { +misskeyApi('hashtags/list', { sort: '+attachedLocalUsers', attachedToLocalUserOnly: true, limit: 30, }).then(tags => { tagsLocal.value = tags; }); -os.api('hashtags/list', { +misskeyApi('hashtags/list', { sort: '+attachedRemoteUsers', attachedToRemoteUserOnly: true, limit: 30, diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index 21e6d00613..73ed4945d2 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -38,13 +38,14 @@ import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkTextarea from '@/components/MkTextarea.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const PRESET_DEFAULT = `/// @ 0.16.0 @@ -369,7 +370,7 @@ const flash = ref<Misskey.entities.Flash | null>(null); const visibility = ref<Misskey.entities.FlashUpdateRequest['visibility']>('public'); if (props.id) { - flash.value = await os.api('flash/show', { + flash.value = await misskeyApi('flash/show', { flashId: props.id, }); } diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index 2b9346fcac..9f8956eb8e 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -42,9 +42,9 @@ import { computed, ref } from 'vue'; import MkFlashPreview from '@/components/MkFlashPreview.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; -import { useRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index 5fae1248e9..63478edd19 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -62,12 +62,13 @@ import * as Misskey from 'misskey-js'; import { Interpreter, Parser, values } from '@syuilo/aiscript'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { url } from '@/config.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkAsUi from '@/components/MkAsUi.vue'; import { AsUiComponent, AsUiRoot, registerAsUiLib } from '@/scripts/aiscript/ui.js'; -import { createAiScriptEnv } from '@/scripts/aiscript/api.js'; +import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; import MkFolder from '@/components/MkFolder.vue'; import MkCode from '@/components/MkCode.vue'; import { defaultStore } from '@/store.js'; @@ -84,7 +85,7 @@ const error = ref<any>(null); function fetchFlash() { flash.value = null; - os.api('flash/show', { + misskeyApi('flash/show', { flashId: props.id, }).then(_flash => { flash.value = _flash; @@ -162,15 +163,7 @@ async function run() { THIS_ID: values.STR(flash.value.id), THIS_URL: values.STR(`${url}/play/${flash.value.id}`), }, { - in: (q) => { - return new Promise(ok => { - os.inputText({ - title: q, - }).then(({ result: a }) => { - ok(a ?? ''); - }); - }); - }, + in: aiScriptReadline, out: (value) => { // nop }, diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index d750664221..f7b756ff3f 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -41,7 +41,7 @@ import { shallowRef, computed } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import { userPage, acct } from '@/filters/user.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { infoImageUrl } from '@/instance.js'; @@ -54,13 +54,13 @@ const pagination = { }; function accept(user) { - os.api('following/requests/accept', { userId: user.id }).then(() => { + misskeyApi('following/requests/accept', { userId: user.id }).then(() => { paginationComponent.value.reload(); }); } function reject(user) { - os.api('following/requests/reject', { userId: user.id }).then(() => { + misskeyApi('following/requests/reject', { userId: user.id }).then(() => { paginationComponent.value.reload(); }); } diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue index a0a4a480b5..eefef828bd 100644 --- a/packages/frontend/src/pages/follow.vue +++ b/packages/frontend/src/pages/follow.vue @@ -12,9 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only import { } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; -import { mainRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from "@/store.js"; +import { defaultStore } from '@/store.js'; +import { mainRouter } from '@/global/router/main.js'; async function follow(user): Promise<void> { const { canceled } = await os.confirm({ @@ -42,7 +43,7 @@ if (acct == null) { let promise; if (acct.startsWith('https://')) { - promise = os.api('ap/show', { + promise = misskeyApi('ap/show', { uri: acct, }); promise.then(res => { @@ -60,7 +61,7 @@ if (acct.startsWith('https://')) { } }); } else { - promise = os.api('users/show', Misskey.acct.parse(acct)); + promise = misskeyApi('users/show', Misskey.acct.parse(acct)); promise.then(user => { follow(user); }); diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index 857317c48f..7dfa2b592e 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -47,9 +47,10 @@ import MkSwitch from '@/components/MkSwitch.vue'; import FormSuspense from '@/components/form/suspense.vue'; import { selectFiles } from '@/scripts/select-file.js'; import * as os from '@/os.js'; -import { useRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -107,7 +108,7 @@ async function del() { } watch(() => props.postId, () => { - init.value = () => props.postId ? os.api('gallery/posts/show', { + init.value = () => props.postId ? misskeyApi('gallery/posts/show', { postId: props.postId, }).then(post => { files.value = post.files ?? []; diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index 936d9b8393..57a282e61e 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -53,7 +53,7 @@ import MkPagination from '@/components/MkPagination.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index 54a8790ef9..e84959dd5a 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -66,18 +66,19 @@ import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; import { url } from '@/config.js'; -import { useRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { defaultStore } from '@/store.js'; import { $i } from '@/account.js'; import { isSupportShare } from '@/scripts/navigator.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -97,7 +98,7 @@ const otherPostsPagination = { function fetchPost() { post.value = null; - os.api('gallery/posts/show', { + misskeyApi('gallery/posts/show', { postId: props.postId, }).then(_post => { post.value = _post; diff --git a/packages/frontend/src/pages/install-extentions.vue b/packages/frontend/src/pages/install-extentions.vue index 7e6c75ac99..c42a17e846 100644 --- a/packages/frontend/src/pages/install-extentions.vue +++ b/packages/frontend/src/pages/install-extentions.vue @@ -105,6 +105,7 @@ import MkInfo from '@/components/MkInfo.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { AiScriptPluginMeta, parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js'; import { parseThemeCode, installTheme } from '@/scripts/install-theme.js'; import { unisonReload } from '@/scripts/unison-reload.js'; @@ -159,7 +160,7 @@ async function fetch() { uiPhase.value = 'error'; return; } - const res = await os.api('fetch-external-resources', { + const res = await misskeyApi('fetch-external-resources', { url: url.value, hash: hash.value, }).catch((err) => { diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 683a31c36d..ccda42f700 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -130,6 +130,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import number from '@/filters/number.js'; import { iAmModerator, iAmAdmin } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -166,9 +167,9 @@ const usersPagination = { async function fetch(): Promise<void> { if (iAmAdmin) { - meta.value = await os.api('admin/meta'); + meta.value = await misskeyApi('admin/meta'); } - instance.value = await os.api('federation/show-instance', { + instance.value = await misskeyApi('federation/show-instance', { host: props.host, }); suspended.value = instance.value?.isSuspended ?? false; @@ -182,7 +183,7 @@ async function toggleBlock(): Promise<void> { if (!meta.value) throw new Error('No meta?'); if (!instance.value) throw new Error('No instance?'); const { host } = instance.value; - await os.api('admin/update-meta', { + await misskeyApi('admin/update-meta', { blockedHosts: isBlocked.value ? meta.value.blockedHosts.concat([host]) : meta.value.blockedHosts.filter(x => x !== host), }); } @@ -192,14 +193,14 @@ async function toggleSilenced(): Promise<void> { if (!instance.value) throw new Error('No instance?'); const { host } = instance.value; const silencedHosts = meta.value.silencedHosts ?? []; - await os.api('admin/update-meta', { + await misskeyApi('admin/update-meta', { silencedHosts: isSilenced.value ? silencedHosts.concat([host]) : silencedHosts.filter(x => x !== host), }); } async function toggleSuspend(): Promise<void> { if (!instance.value) throw new Error('No instance?'); - await os.api('admin/federation/update-instance', { + await misskeyApi('admin/federation/update-instance', { host: instance.value.host, isSuspended: suspended.value, }); @@ -207,7 +208,7 @@ async function toggleSuspend(): Promise<void> { async function toggleNSFW(): Promise<void> { if (!instance.value) throw new Error('No instance?'); - await os.api('admin/federation/update-instance', { + await misskeyApi('admin/federation/update-instance', { host: instance.value.host, isNSFW: isNSFW.value, }); @@ -215,7 +216,7 @@ async function toggleNSFW(): Promise<void> { function refreshMetadata(): void { if (!instance.value) throw new Error('No instance?'); - os.api('admin/federation/refresh-remote-instance-metadata', { + misskeyApi('admin/federation/refresh-remote-instance-metadata', { host: instance.value.host, }); os.alert({ diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue index 6ac78a2068..d20ec14118 100644 --- a/packages/frontend/src/pages/invite.vue +++ b/packages/frontend/src/pages/invite.vue @@ -40,6 +40,7 @@ import { computed, ref, shallowRef } from 'vue'; import type * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkButton from '@/components/MkButton.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkInviteCode from '@/components/MkInviteCode.vue'; @@ -68,7 +69,7 @@ const resetCycle = computed<null | string>(() => { }); async function create() { - const ticket = await os.api('invite/create'); + const ticket = await misskeyApi('invite/create'); os.alert({ type: 'success', title: i18n.ts.inviteCodeCreated, @@ -87,7 +88,7 @@ function deleted(id: string) { } async function update() { - currentInviteLimit.value = (await os.api('invite/limit')).remaining; + currentInviteLimit.value = (await misskeyApi('invite/limit')).remaining; } update(); diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue index d6c6685a79..7cf5025eaa 100644 --- a/packages/frontend/src/pages/list.vue +++ b/packages/frontend/src/pages/list.vue @@ -37,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { watch, computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { userPage } from '@/filters/user.js'; import { i18n } from '@/i18n.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; @@ -53,12 +54,12 @@ const error = ref(); const users = ref<Misskey.entities.UserDetailed[]>([]); function fetchList(): void { - os.api('users/lists/show', { + misskeyApi('users/lists/show', { listId: props.listId, forPublic: true, }).then(_list => { list.value = _list; - os.api('users/show', { + misskeyApi('users/show', { userIds: list.value.userIds, }).then(_users => { users.value = _users; diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue index 2b53b67ab3..333b485c70 100644 --- a/packages/frontend/src/pages/miauth.vue +++ b/packages/frontend/src/pages/miauth.vue @@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed } from 'vue'; import MkSignin from '@/components/MkSignin.vue'; import MkButton from '@/components/MkButton.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i, login } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -65,7 +65,7 @@ const state = ref<string | null>(null); async function accept(): Promise<void> { state.value = 'waiting'; - await os.api('miauth/gen-token', { + await misskeyApi('miauth/gen-token', { session: props.session, name: props.name, iconUrl: props.icon, diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue index 79b592dada..c1f8064c23 100644 --- a/packages/frontend/src/pages/my-antennas/create.vue +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -14,8 +14,8 @@ import { ref } from 'vue'; import XAntenna from './editor.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { useRouter } from '@/router.js'; import { antennasCache } from '@/cache.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue index 851b32527c..38bfa9f0cb 100644 --- a/packages/frontend/src/pages/my-antennas/edit.vue +++ b/packages/frontend/src/pages/my-antennas/edit.vue @@ -13,11 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import XAntenna from './editor.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { useRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { antennasCache } from '@/cache.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -32,7 +32,7 @@ function onAntennaUpdated() { router.push('/my/antennas'); } -os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => { +misskeyApi('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => { antenna.value = antennaResponse; }); diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue index 0fc7f862a3..a002286ca4 100644 --- a/packages/frontend/src/pages/my-antennas/editor.vue +++ b/packages/frontend/src/pages/my-antennas/editor.vue @@ -57,6 +57,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -84,7 +85,7 @@ const userLists = ref<Misskey.entities.UserList[] | null>(null); watch(() => src.value, async () => { if (src.value === 'list' && userLists.value === null) { - userLists.value = await os.api('users/lists/list'); + userLists.value = await misskeyApi('users/lists/list'); } }); @@ -119,7 +120,7 @@ async function deleteAntenna() { }); if (canceled) return; - await os.api('antennas/delete', { + await misskeyApi('antennas/delete', { antennaId: props.antenna.id, }); diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index d787e53bb0..c4d1f9655b 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -32,6 +32,7 @@ import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import MkClipPreview from '@/components/MkClipPreview.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { clipsCache } from '@/cache.js'; @@ -48,7 +49,7 @@ const favorites = ref<Misskey.entities.Clip[] | null>(null); const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); watch(tab, async () => { - favorites.value = await os.api('clips/my-favorites'); + favorites.value = await misskeyApi('clips/my-favorites'); }); async function create() { diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue index 3379cf43d4..25a54375ff 100644 --- a/packages/frontend/src/pages/my-lists/index.vue +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="items.length > 0" class="_gaps"> <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> - <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }})</span></div> + <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div> <MkAvatars :userIds="list.userIds" :limit="10"/> </MkA> </div> @@ -37,7 +37,9 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { userListsCache } from '@/cache.js'; import { infoImageUrl } from '@/instance.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; + +const $i = signinRequired(); const items = computed(() => userListsCache.value.value ?? []); diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index df9cdb0fce..4d1e0b4874 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder defaultOpen> <template #label>{{ i18n.ts.members }}</template> - <template #caption>{{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }}</template> + <template #caption>{{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template> <div class="_gaps_s"> <MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton> @@ -57,7 +57,7 @@ import { computed, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; -import { mainRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import { userPage } from '@/filters/user.js'; @@ -66,9 +66,12 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkInput from '@/components/MkInput.vue'; import { userListsCache } from '@/cache.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import { defaultStore } from '@/store.js'; import MkPagination from '@/components/MkPagination.vue'; +import { mainRouter } from '@/global/router/main.js'; + +const $i = signinRequired(); const { enableInfiniteScroll, @@ -91,7 +94,7 @@ const membershipsPagination = { }; function fetchList() { - os.api('users/lists/show', { + misskeyApi('users/lists/show', { listId: props.listId, }).then(_list => { list.value = _list; @@ -119,7 +122,7 @@ async function removeUser(item, ev) { danger: true, action: async () => { if (!list.value) return; - os.api('users/lists/pull', { + misskeyApi('users/lists/pull', { listId: list.value.id, userId: item.userId, }).then(() => { @@ -134,7 +137,7 @@ async function showMembershipMenu(item, ev) { text: item.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline, icon: item.withReplies ? 'ph-envelope-open ph-bold ph-lg' : 'ph-envelope ph-bold ph-lg', action: async () => { - os.api('users/lists/update-membership', { + misskeyApi('users/lists/update-membership', { listId: list.value.id, userId: item.userId, withReplies: !item.withReplies, diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index a98a7bde2c..6cc91f417b 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -55,7 +55,7 @@ import MkNotes from '@/components/MkNotes.vue'; import SkNoteDetailed from '@/components/SkNoteDetailed.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; import MkButton from '@/components/MkButton.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import { dateString } from '@/filters/date.js'; @@ -96,13 +96,13 @@ function fetchNote() { showPrev.value = false; showNext.value = false; note.value = null; - os.api('notes/show', { + misskeyApi('notes/show', { noteId: props.noteId, }).then(res => { note.value = res; // 古いノートは被クリップ数をカウントしていないので、2023-10-01以前のものは強制的にnotes/clipsを叩く if (note.value.clippedCount > 0 || new Date(note.value.createdAt).getTime() < new Date('2023-10-01').getTime()) { - os.api('notes/clips', { + misskeyApi('notes/clips', { noteId: note.value.id, }).then((_clips) => { clips.value = _clips; diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue index 459454a9be..4637c20fce 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue @@ -26,6 +26,7 @@ import * as Misskey from 'misskey-js'; import XContainer from '../page-editor.container.vue'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -52,7 +53,7 @@ onMounted(async () => { if (props.modelValue.fileId == null) { await choose(); } else { - os.api('drive/files/show', { + misskeyApi('drive/files/show', { fileId: props.modelValue.fileId, }).then(fileResponse => { file.value = fileResponse; diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue index 442558cc2a..b82e57fe42 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue @@ -30,7 +30,7 @@ import MkInput from '@/components/MkInput.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkNote from '@/components/MkNote.vue'; import MkNoteDetailed from '@/components/MkNoteDetailed.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -53,7 +53,7 @@ watch(id, async () => { ...props.modelValue, note: id.value, }); - note.value = await os.api('notes/show', { noteId: id.value }); + note.value = await misskeyApi('notes/show', { noteId: id.value }); }, { immediate: true, }); diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index 8c4696b04b..ee17be3c42 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -70,11 +70,12 @@ import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { selectFile } from '@/scripts/select-file.js'; -import { mainRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { $i } from '@/account.js'; +import { mainRouter } from '@/global/router/main.js'; const props = defineProps<{ initPageId?: string; @@ -105,7 +106,7 @@ watch(eyeCatchingImageId, async () => { if (eyeCatchingImageId.value == null) { eyeCatchingImage.value = null; } else { - eyeCatchingImage.value = await os.api('drive/files/show', { + eyeCatchingImage.value = await misskeyApi('drive/files/show', { fileId: eyeCatchingImageId.value, }); } @@ -148,7 +149,7 @@ function save() { if (pageId.value) { options.pageId = pageId.value; - os.api('pages/update', options) + misskeyApi('pages/update', options) .then(page => { currentName.value = name.value.trim(); os.alert({ @@ -157,7 +158,7 @@ function save() { }); }).catch(onError); } else { - os.api('pages/create', options) + misskeyApi('pages/create', options) .then(created => { pageId.value = created.id; currentName.value = name.value.trim(); @@ -176,7 +177,7 @@ function del() { text: i18n.t('removeAreYouSure', { x: title.value.trim() }), }).then(({ canceled }) => { if (canceled) return; - os.api('pages/delete', { + misskeyApi('pages/delete', { pageId: pageId.value, }).then(() => { os.alert({ @@ -191,7 +192,7 @@ function del() { function duplicate() { title.value = title.value + ' - copy'; name.value = name.value + '-copy'; - os.api('pages/create', getSaveOptions()).then(created => { + misskeyApi('pages/create', getSaveOptions()).then(created => { pageId.value = created.id; currentName.value = name.value.trim(); os.alert({ @@ -235,11 +236,11 @@ function removeEyeCatchingImage() { async function init() { if (props.initPageId) { - page.value = await os.api('pages/show', { + page.value = await misskeyApi('pages/show', { pageId: props.initPageId, }); } else if (props.initPageName && props.initUser) { - page.value = await os.api('pages/show', { + page.value = await misskeyApi('pages/show', { name: props.initPageName, username: props.initUser, }); diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 6b06da9a24..d6f25235ec 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -81,6 +81,7 @@ import * as Misskey from 'misskey-js'; import XPage from '@/components/page/page.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { url } from '@/config.js'; import MkMediaImage from '@/components/MkMediaImage.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; @@ -113,7 +114,7 @@ const path = computed(() => props.username + '/' + props.pageName); function fetchPage() { page.value = null; - os.api('pages/show', { + misskeyApi('pages/show', { name: props.pageName, username: props.username, }).then(async _page => { diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue index a7ca433ed3..033ad1a48e 100644 --- a/packages/frontend/src/pages/pages.vue +++ b/packages/frontend/src/pages/pages.vue @@ -40,9 +40,9 @@ import { computed, ref } from 'vue'; import MkPagePreview from '@/components/MkPagePreview.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; -import { useRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index 95aa64f8d3..29df0de305 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -36,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { watch, computed, ref } from 'vue'; import JSON5 from 'json5'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import FormLink from '@/components/form/link.vue'; @@ -54,7 +55,7 @@ const scope = computed(() => props.path ? props.path.split('/') : []); const keys = ref<any>(null); function fetchKeys() { - os.api('i/registry/keys-with-type', { + misskeyApi('i/registry/keys-with-type', { scope: scope.value, domain: props.domain === '@' ? null : props.domain, }).then(res => { diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue index fb3cc4a556..444aa1511b 100644 --- a/packages/frontend/src/pages/registry.value.vue +++ b/packages/frontend/src/pages/registry.value.vue @@ -48,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { watch, computed, ref } from 'vue'; import JSON5 from 'json5'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; @@ -68,7 +69,7 @@ const value = ref<any>(null); const valueForEditor = ref<string | null>(null); function fetchValue() { - os.api('i/registry/get-detail', { + misskeyApi('i/registry/get-detail', { scope: scope.value, key: key.value, domain: props.domain === '@' ? null : props.domain, diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue index 7d1dd751ab..06c36456e4 100644 --- a/packages/frontend/src/pages/registry.vue +++ b/packages/frontend/src/pages/registry.vue @@ -26,6 +26,7 @@ import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import JSON5 from 'json5'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import FormLink from '@/components/form/link.vue'; @@ -35,7 +36,7 @@ import MkButton from '@/components/MkButton.vue'; const scopesWithDomain = ref<Misskey.entities.IRegistryScopesWithDomainResponse | null>(null); function fetchScopes() { - os.api('i/registry/scopes-with-domain').then(res => { + misskeyApi('i/registry/scopes-with-domain').then(res => { scopesWithDomain.value = res; }); } diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue index 1aed57724e..6263c9a460 100644 --- a/packages/frontend/src/pages/reset-password.vue +++ b/packages/frontend/src/pages/reset-password.vue @@ -25,8 +25,8 @@ import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { mainRouter } from '@/global/router/main.js'; const props = defineProps<{ token?: string; diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index 6dce4f187d..631182bbce 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkUserList from '@/components/MkUserList.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; @@ -59,7 +59,7 @@ const error = ref(); const visible = ref(false); watch(() => props.role, () => { - os.api('roles/show', { + misskeyApi('roles/show', { roleId: props.role, }).then(res => { role.value = res; diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue index ccda5bb8ac..a80726a4d8 100644 --- a/packages/frontend/src/pages/scratchpad.vue +++ b/packages/frontend/src/pages/scratchpad.vue @@ -44,7 +44,7 @@ import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import MkContainer from '@/components/MkContainer.vue'; import MkButton from '@/components/MkButton.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue'; -import { createAiScriptEnv } from '@/scripts/aiscript/api.js'; +import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; import * as os from '@/os.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; @@ -86,19 +86,7 @@ async function run() { root.value = _root.value; }), }), { - in: (q) => { - return new Promise(ok => { - os.inputText({ - title: q, - }).then(({ canceled, result: a }) => { - if (canceled) { - ok(''); - } else { - ok(a); - } - }); - }); - }, + in: aiScriptReadline, out: (value) => { if (value.type === 'str' && value.value.toLowerCase().replace(',', '').includes('hello world')) { claimAchievement('outputHelloWorldOnScratchpad'); diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index 405db06758..8ce84de06c 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -58,9 +58,10 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; -import { useRouter } from '@/router.js'; import MkFolder from '@/components/MkFolder.vue'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -85,7 +86,7 @@ async function search() { if (query == null || query === '') return; if (query.startsWith('https://')) { - const promise = os.api('ap/show', { + const promise = misskeyApi('ap/show', { uri: query, }); diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index 596f4da711..ac3380bc5f 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -33,7 +33,8 @@ import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; -import { useRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -48,7 +49,7 @@ async function search() { if (query == null || query === '') return; if (query.startsWith('https://')) { - const promise = os.api('ap/show', { + const promise = misskeyApi('ap/show', { uri: query, }); diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue index 9a2a98ad89..ff0b048df5 100644 --- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue +++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue @@ -110,7 +110,9 @@ import * as os from '@/os.js'; import MkFolder from '@/components/MkFolder.vue'; import MkInfo from '@/components/MkInfo.vue'; import { confetti } from '@/scripts/confetti.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; + +const $i = signinRequired(); defineProps<{ twoFactorData: { @@ -151,7 +153,7 @@ function downloadBackupCodes() { const txtBlob = new Blob([backupCodes.value.join('\n')], { type: 'text/plain' }); const dummya = document.createElement('a'); dummya.href = URL.createObjectURL(txtBlob); - dummya.download = `${$i?.username}-2fa-backup-codes.txt`; + dummya.download = `${$i.username}-2fa-backup-codes.txt`; dummya.click(); } } diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index 09421ba2c2..1f2b925b53 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -80,9 +80,11 @@ import MkSwitch from '@/components/MkSwitch.vue'; import FormSection from '@/components/form/section.vue'; import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import { i18n } from '@/i18n.js'; +const $i = signinRequired(); + // メモ: 各エンドポイントはmeUpdatedを発行するため、refreshAccountは不要 withDefaults(defineProps<{ @@ -91,7 +93,7 @@ withDefaults(defineProps<{ first: false, }); -const usePasswordLessLogin = computed(() => $i?.usePasswordLessLogin ?? false); +const usePasswordLessLogin = computed(() => $i.usePasswordLessLogin ?? false); async function registerTOTP(): Promise<void> { const auth = await os.authenticateDialog(); diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue index 697ce27f2f..379fe2d366 100644 --- a/packages/frontend/src/pages/settings/accounts.vue +++ b/packages/frontend/src/pages/settings/accounts.vue @@ -24,6 +24,7 @@ import type * as Misskey from 'misskey-js'; import FormSuspense from '@/components/form/suspense.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -36,7 +37,7 @@ const init = async () => { getAccounts().then(accounts => { storedAccounts.value = accounts.filter(x => x.id !== $i!.id); - return os.api('users/show', { + return misskeyApi('users/show', { userIds: storedAccounts.value.map(x => x.id), }); }).then(response => { diff --git a/packages/frontend/src/pages/settings/api.vue b/packages/frontend/src/pages/settings/api.vue index ca38bd2e3d..b3d1cab313 100644 --- a/packages/frontend/src/pages/settings/api.vue +++ b/packages/frontend/src/pages/settings/api.vue @@ -16,6 +16,7 @@ import { defineAsyncComponent, ref, computed } from 'vue'; import FormLink from '@/components/form/link.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -25,7 +26,7 @@ function generateToken() { os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {}, { done: async result => { const { name, permissions } = result; - const { token } = await os.api('miauth/gen-token', { + const { token } = await misskeyApi('miauth/gen-token', { session: null, name: name, permission: permissions, diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue index f492dc6d31..690e4edb76 100644 --- a/packages/frontend/src/pages/settings/apps.vue +++ b/packages/frontend/src/pages/settings/apps.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed } from 'vue'; import FormPagination from '@/components/MkPagination.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkKeyValue from '@/components/MkKeyValue.vue'; @@ -66,7 +66,7 @@ const pagination = { }; function revoke(token) { - os.api('i/revoke-token', { tokenId: token.id }).then(() => { + misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => { list.value.reload(); }); } diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue index 2bf261abd9..46783505b6 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue @@ -16,7 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; + +const $i = signinRequired(); const props = defineProps<{ active?: boolean; diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue index a46a92d1c6..79aaa4afd0 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue @@ -51,7 +51,9 @@ import MkModalWindow from '@/components/MkModalWindow.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import { i18n } from '@/i18n.js'; import MkRange from '@/components/MkRange.vue'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; + +const $i = signinRequired(); const props = defineProps<{ usingIndex: number | null; diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue index 976f6aa68c..8a2a68b9b1 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.vue @@ -50,15 +50,18 @@ import * as Misskey from 'misskey-js'; import XDecoration from './avatar-decoration.decoration.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import MkInfo from '@/components/MkInfo.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +const $i = signinRequired(); + const loading = ref(true); const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse>([]); -os.api('get-avatar-decorations').then(_avatarDecorations => { +misskeyApi('get-avatar-decorations').then(_avatarDecorations => { avatarDecorations.value = _avatarDecorations; loading.value = false; }); diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue index 601479b73c..5e6c728d50 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -51,6 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, ref, watch } from 'vue'; import tinycolor from 'tinycolor2'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkPagination from '@/components/MkPagination.vue'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import { i18n } from '@/i18n.js'; @@ -94,7 +95,7 @@ watch(sortModeSelect, () => { function fetchDriveInfo(): void { fetching.value = true; - os.api('drive').then(info => { + misskeyApi('drive').then(info => { capacity.value = info.capacity; usage.value = info.usage; fetching.value = false; diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index 166e49ac54..89e921f6cb 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -62,12 +62,15 @@ import FormSection from '@/components/form/section.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import bytes from '@/filters/bytes.js'; import { defaultStore } from '@/store.js'; import MkChart from '@/components/MkChart.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; + +const $i = signinRequired(); const fetching = ref(true); const usage = ref<number | null>(null); @@ -76,6 +79,7 @@ const uploadFolder = ref<Misskey.entities.DriveFolder | null>(null); const alwaysMarkNsfw = ref($i.alwaysMarkNsfw); const meterStyle = computed(() => { + if (!capacity.value || !usage.value) return {}; return { width: `${usage.value / capacity.value * 100}%`, background: tinycolor({ @@ -88,14 +92,14 @@ const meterStyle = computed(() => { const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading')); -os.api('drive').then(info => { +misskeyApi('drive').then(info => { capacity.value = info.capacity; usage.value = info.usage; fetching.value = false; }); if (defaultStore.state.uploadFolder) { - os.api('drive/folders/show', { + misskeyApi('drive/folders/show', { folderId: defaultStore.state.uploadFolder, }).then(response => { uploadFolder.value = response; @@ -107,7 +111,7 @@ function chooseUploadFolder() { defaultStore.set('uploadFolder', folder ? folder.id : null); os.success(); if (defaultStore.state.uploadFolder) { - uploadFolder.value = await os.api('drive/folders/show', { + uploadFolder.value = await misskeyApi('drive/folders/show', { folderId: defaultStore.state.uploadFolder, }); } else { @@ -117,7 +121,7 @@ function chooseUploadFolder() { } function saveProfile() { - os.api('i/update', { + misskeyApi('i/update', { alwaysMarkNsfw: !!alwaysMarkNsfw.value, }).catch(err => { os.alert({ diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue index 003501f45a..ea2ab07a85 100644 --- a/packages/frontend/src/pages/settings/email.vue +++ b/packages/frontend/src/pages/settings/email.vue @@ -54,15 +54,18 @@ import MkInfo from '@/components/MkInfo.vue'; import MkInput from '@/components/MkInput.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; -import { $i } from '@/account.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { signinRequired } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { instance } from '@/instance.js'; -const emailAddress = ref($i!.email); +const $i = signinRequired(); + +const emailAddress = ref($i.email); const onChangeReceiveAnnouncementEmail = (v) => { - os.api('i/update', { + misskeyApi('i/update', { receiveAnnouncementEmail: v, }); }; @@ -78,14 +81,14 @@ async function saveEmailAddress() { }); } -const emailNotification_mention = ref($i!.emailNotificationTypes.includes('mention')); -const emailNotification_reply = ref($i!.emailNotificationTypes.includes('reply')); -const emailNotification_quote = ref($i!.emailNotificationTypes.includes('quote')); -const emailNotification_follow = ref($i!.emailNotificationTypes.includes('follow')); -const emailNotification_receiveFollowRequest = ref($i!.emailNotificationTypes.includes('receiveFollowRequest')); +const emailNotification_mention = ref($i.emailNotificationTypes.includes('mention')); +const emailNotification_reply = ref($i.emailNotificationTypes.includes('reply')); +const emailNotification_quote = ref($i.emailNotificationTypes.includes('quote')); +const emailNotification_follow = ref($i.emailNotificationTypes.includes('follow')); +const emailNotification_receiveFollowRequest = ref($i.emailNotificationTypes.includes('receiveFollowRequest')); const saveNotificationSettings = () => { - os.api('i/update', { + misskeyApi('i/update', { emailNotificationTypes: [ ...[emailNotification_mention.value ? 'mention' : null], ...[emailNotification_reply.value ? 'reply' : null], diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index f0644aae80..b4e3680216 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -275,6 +275,7 @@ import MkInfo from '@/components/MkInfo.vue'; import { langs } from '@/config.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -438,7 +439,7 @@ function removeEmojiIndex(lang: string) { } async function setPinnedList() { - const lists = await os.api('users/lists/list'); + const lists = await misskeyApi('users/lists/list'); const { canceled, result: list } = await os.select({ title: i18n.ts.selectList, items: lists.map(x => ({ diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index 7ca1faf406..35aef375c0 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -37,6 +37,14 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> </FormSection> <FormSection> + <template #label><i class="ph-clip ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.clips }}</template> + <MkFolder> + <template #label>{{ i18n.ts.export }}</template> + <template #icon><i class="ph-download ph-bold ph-lg"></i></template> + <MkButton primary :class="$style.button" inline @click="exportClips()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton> + </MkFolder> + </FormSection> + <FormSection> <template #label><i class="ph-users ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.followingList }}</template> <div class="_gaps_s"> <MkFolder> @@ -133,11 +141,12 @@ import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkRadios from '@/components/MkRadios.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { selectFile } from '@/scripts/select-file.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { $i } from '@/account.js'; -import { defaultStore } from "@/store.js"; +import { defaultStore } from '@/store.js'; const excludeMutingUsers = ref(false); const excludeInactiveUsers = ref(false); @@ -166,15 +175,19 @@ const onError = (ev) => { }; const exportNotes = () => { - os.api('i/export-notes', {}).then(onExportSuccess).catch(onError); + misskeyApi('i/export-notes', {}).then(onExportSuccess).catch(onError); }; const exportFavorites = () => { - os.api('i/export-favorites', {}).then(onExportSuccess).catch(onError); + misskeyApi('i/export-favorites', {}).then(onExportSuccess).catch(onError); +}; + +const exportClips = () => { + misskeyApi('i/export-clips', {}).then(onExportSuccess).catch(onError); }; const exportFollowing = () => { - os.api('i/export-following', { + misskeyApi('i/export-following', { excludeMuting: excludeMutingUsers.value, excludeInactive: excludeInactiveUsers.value, }) @@ -182,24 +195,24 @@ const exportFollowing = () => { }; const exportBlocking = () => { - os.api('i/export-blocking', {}).then(onExportSuccess).catch(onError); + misskeyApi('i/export-blocking', {}).then(onExportSuccess).catch(onError); }; const exportUserLists = () => { - os.api('i/export-user-lists', {}).then(onExportSuccess).catch(onError); + misskeyApi('i/export-user-lists', {}).then(onExportSuccess).catch(onError); }; const exportMuting = () => { - os.api('i/export-mute', {}).then(onExportSuccess).catch(onError); + misskeyApi('i/export-mute', {}).then(onExportSuccess).catch(onError); }; const exportAntennas = () => { - os.api('i/export-antennas', {}).then(onExportSuccess).catch(onError); + misskeyApi('i/export-antennas', {}).then(onExportSuccess).catch(onError); }; const importFollowing = async (ev) => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-following', { + misskeyApi('i/import-following', { fileId: file.id, withReplies: withReplies.value, }).then(onImportSuccess).catch(onError); @@ -207,7 +220,7 @@ const importFollowing = async (ev) => { const importNotes = async (ev) => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-notes', { + misskeyApi('i/import-notes', { fileId: file.id, type: noteType.value, }).then(onImportSuccess).catch(onError); @@ -215,22 +228,22 @@ const importNotes = async (ev) => { const importUserLists = async (ev) => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError); + misskeyApi('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importMuting = async (ev) => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError); + misskeyApi('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importBlocking = async (ev) => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); + misskeyApi('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importAntennas = async (ev) => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError); + misskeyApi('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 96575e097b..bcdce7bb68 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -34,9 +34,9 @@ import MkSuperMenu from '@/components/MkSuperMenu.vue'; import { signout, $i } from '@/account.js'; import { clearCache } from '@/scripts/clear-cache.js'; import { instance } from '@/instance.js'; -import { useRouter } from '@/router.js'; import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import * as os from '@/os.js'; +import { useRouter } from '@/global/router/supplier.js'; const indexInfo = { title: i18n.ts.settings, diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue index 3b47189eb4..a0f27f356b 100644 --- a/packages/frontend/src/pages/settings/migration.vue +++ b/packages/frontend/src/pages/settings/migration.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkFolder> - <MkFolder :defaultOpen="!!$i?.movedTo"> + <MkFolder :defaultOpen="!!$i.movedTo"> <template #icon><i class="ph-airplane-takeoff ph-bold ph-lg"></i></template> <template #label>{{ i18n.ts._accountMigration.moveTo }}</template> @@ -66,24 +66,27 @@ import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkUserInfo from '@/components/MkUserInfo.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import { unisonReload } from '@/scripts/unison-reload.js'; +const $i = signinRequired(); + const moveToAccount = ref(''); const movedTo = ref<Misskey.entities.UserDetailed>(); const accountAliases = ref(['']); async function init() { - if ($i?.movedTo) { - movedTo.value = await os.api('users/show', { userId: $i.movedTo }); + if ($i.movedTo) { + movedTo.value = await misskeyApi('users/show', { userId: $i.movedTo }); } else { moveToAccount.value = ''; } - if ($i?.alsoKnownAs && $i.alsoKnownAs.length > 0) { - const alsoKnownAs = await os.api('users/show', { userIds: $i.alsoKnownAs }); + if ($i.alsoKnownAs && $i.alsoKnownAs.length > 0) { + const alsoKnownAs = await misskeyApi('users/show', { userIds: $i.alsoKnownAs }); accountAliases.value = (alsoKnownAs && alsoKnownAs.length > 0) ? alsoKnownAs.map(user => `@${Misskey.acct.toString(user)}`) : ['']; } else { accountAliases.value = ['']; diff --git a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue index 0e149fd461..a5b7f7a79c 100644 --- a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue +++ b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue @@ -19,11 +19,13 @@ import { ref, watch } from 'vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkButton from '@/components/MkButton.vue'; -import * as os from '@/os.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -const instanceMutes = ref($i!.mutedInstances.join('\n')); +const $i = signinRequired(); + +const instanceMutes = ref($i.mutedInstances.join('\n')); const changed = ref(false); async function save() { @@ -32,7 +34,7 @@ async function save() { .map(el => el.trim()) .filter(el => el); - await os.api('i/update', { + await misskeyApi('i/update', { mutedInstances: mutes, }); diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index a996a03cce..2535ab6344 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -9,14 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ph-envelope ph-bold ph-lg"></i></template> <template #label>{{ i18n.ts.wordMute }}</template> - <XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/> + <XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/> </MkFolder> <MkFolder> <template #icon><i class="ph-x-square ph-bold ph-lg"></i></template> <template #label>{{ i18n.ts.hardWordMute }}</template> - <XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/> + <XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/> </MkFolder> <MkFolder> @@ -136,9 +136,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import * as os from '@/os.js'; import { infoImageUrl } from '@/instance.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import MkFolder from '@/components/MkFolder.vue'; +const $i = signinRequired(); + const renoteMutingPagination = { endpoint: 'renote-mute/list' as const, limit: 10, diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index 0bdfbdf741..d257713e9a 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -62,18 +62,21 @@ import FormSection from '@/components/form/section.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; import { notificationTypes } from '@/const.js'; +const $i = signinRequired(); + const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned']; const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>(); const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer); const sendReadMessage = computed(() => pushRegistrationInServer.value?.sendReadMessage || false); -const userLists = await os.api('users/lists/list'); +const userLists = await misskeyApi('users/lists/list'); async function readAllUnreadNotes() { await os.apiWithDialog('i/read-all-unread-notes'); @@ -86,11 +89,11 @@ async function readAllNotifications() { async function updateReceiveConfig(type, value) { await os.apiWithDialog('i/update', { notificationRecieveConfig: { - ...$i!.notificationRecieveConfig, + ...$i.notificationRecieveConfig, [type]: value, }, }).then(i => { - $i!.notificationRecieveConfig = i.notificationRecieveConfig; + $i.notificationRecieveConfig = i.notificationRecieveConfig; }); } @@ -107,7 +110,7 @@ function onChangeSendReadMessage(v: boolean) { } function testNotification(): void { - os.api('notifications/test-notification'); + misskeyApi('notifications/test-notification'); } const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index efda0c00b3..8c7a62c6f8 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -104,26 +104,21 @@ import FormInfo from '@/components/MkInfo.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; -import { signout, $i } from '@/account.js'; +import { signout, signinRequired } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import FormSection from '@/components/form/section.vue'; +const $i = signinRequired(); + const reportError = computed(defaultStore.makeGetterSetter('reportError')); const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct')); const devMode = computed(defaultStore.makeGetterSetter('devMode')); const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); -function onChangeInjectFeaturedNote(v) { - os.api('i/update', { - injectFeaturedNote: v, - }).then((i) => { - $i!.injectFeaturedNote = i.injectFeaturedNote; - }); -} - async function deleteAccount() { { const { canceled } = await os.confirm({ @@ -165,11 +160,11 @@ async function updateRepliesAll(withReplies: boolean) { }); if (canceled) return; - os.api('following/update-all', { withReplies }); + misskeyApi('following/update-all', { withReplies }); } const exportData = () => { - os.api('i/export-data', {}).then(() => { + misskeyApi('i/export-data', {}).then(() => { os.alert({ type: 'info', text: i18n.ts.exportRequested, diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index c7538f3a1b..5fccf15df6 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -43,6 +43,7 @@ import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { useStream } from '@/stream.js'; @@ -146,7 +147,7 @@ const connection = $i && useStream().useChannel('main'); const profiles = ref<Record<string, Profile> | null>(null); -os.api('i/registry/get-all', { scope }) +misskeyApi('i/registry/get-all', { scope }) .then(res => { profiles.value = res || {}; }); diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 62056ff8a6..7dcab1acf8 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -77,12 +77,14 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import FormSection from '@/components/form/section.vue'; import MkFolder from '@/components/MkFolder.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +const $i = signinRequired(); + const isLocked = ref($i.isLocked); const autoAcceptFollowed = ref($i.autoAcceptFollowed); const noCrawle = ref($i.noCrawle); @@ -90,8 +92,8 @@ const noindex = ref($i.noindex); const isExplorable = ref($i.isExplorable); const hideOnlineStatus = ref($i.hideOnlineStatus); const publicReactions = ref($i.publicReactions); -const followingVisibility = ref($i?.followingVisibility); -const followersVisibility = ref($i?.followersVisibility); +const followingVisibility = ref($i.followingVisibility); +const followersVisibility = ref($i.followersVisibility); const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); @@ -99,7 +101,7 @@ const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberN const keepCw = computed(defaultStore.makeGetterSetter('keepCw')); function save() { - os.api('i/update', { + misskeyApi('i/update', { isLocked: !!isLocked.value, autoAcceptFollowed: !!autoAcceptFollowed.value, noCrawle: !!noCrawle.value, diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index f98680bbb6..ce430d97e4 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -127,7 +127,7 @@ import FormSlot from '@/components/form/slot.vue'; import { selectFile } from '@/scripts/select-file.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import { langmap } from '@/scripts/langmap.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { claimAchievement } from '@/scripts/achievements.js'; @@ -135,6 +135,8 @@ import { defaultStore } from '@/store.js'; import MkInfo from '@/components/MkInfo.vue'; import MkTextarea from '@/components/MkTextarea.vue'; +const $i = signinRequired(); + const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance')); @@ -152,10 +154,10 @@ const profile = reactive({ description: $i.description, location: $i.location, birthday: $i.birthday, - listenbrainz: $i?.listenbrainz, + listenbrainz: $i.listenbrainz, lang: $i.lang, - isBot: $i.isBot, - isCat: $i.isCat, + isBot: $i.isBot ?? false, + isCat: $i.isCat ?? false, speakAsCat: $i.speakAsCat, }); @@ -165,7 +167,7 @@ watch(() => profile, () => { deep: true, }); -const fields = ref($i?.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []); +const fields = ref($i.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []); const fieldEditMode = ref(false); function addField() { diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue index 716b168c92..cf6e75cba5 100644 --- a/packages/frontend/src/pages/settings/roles.vue +++ b/packages/frontend/src/pages/settings/roles.vue @@ -27,15 +27,11 @@ import { computed } from 'vue'; import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkRolePreview from '@/components/MkRolePreview.vue'; -function save() { - os.apiWithDialog('i/update', { - - }); -} +const $i = signinRequired(); const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue index 9ae479e6e4..44f98969a8 100644 --- a/packages/frontend/src/pages/settings/security.vue +++ b/packages/frontend/src/pages/settings/security.vue @@ -47,6 +47,7 @@ import FormSlot from '@/components/form/slot.vue'; import MkButton from '@/components/MkButton.vue'; import MkPagination from '@/components/MkPagination.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -92,7 +93,7 @@ async function regenerateToken() { const auth = await os.authenticateDialog(); if (auth.canceled) return; - os.api('i/regenerate-token', { + misskeyApi('i/regenerate-token', { password: auth.result.password, token: auth.result.token, }); diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue index a43ffb1f0b..813a971d3f 100644 --- a/packages/frontend/src/pages/settings/sounds.sound.vue +++ b/packages/frontend/src/pages/settings/sounds.sound.vue @@ -32,6 +32,7 @@ import MkButton from '@/components/MkButton.vue'; import MkRange from '@/components/MkRange.vue'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { playFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js'; import { selectFile } from '@/scripts/select-file.js'; @@ -53,7 +54,7 @@ const fileName = ref<string>(''); const volume = ref(props.volume); if (type.value === '_driveFile_' && fileId.value) { - const apiRes = await os.api('drive/files/show', { + const apiRes = await misskeyApi('drive/files/show', { fileId: fileId.value, }); fileName.value = apiRes.name; diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue index c45e386ac5..4270ef285e 100644 --- a/packages/frontend/src/pages/settings/statusbar.vue +++ b/packages/frontend/src/pages/settings/statusbar.vue @@ -21,7 +21,7 @@ import { v4 as uuid } from 'uuid'; import XStatusbar from './statusbar.statusbar.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -31,7 +31,7 @@ const statusbars = defaultStore.reactiveState.statusbars; const userLists = ref<Misskey.entities.UserList[] | null>(null); onMounted(() => { - os.api('users/lists/list').then(res => { + misskeyApi('users/lists/list').then(res => { userLists.value = res; }); }); diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index f6e2f63317..7dd995fc8a 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -48,9 +48,10 @@ import FormSection from '@/components/form/section.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -58,7 +59,7 @@ const props = defineProps<{ webhookId: string; }>(); -const webhook = await os.api('i/webhooks/show', { +const webhook = await misskeyApi('i/webhooks/show', { webhookId: props.webhookId, }); diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue index a978be0ae5..e2d3362fb8 100644 --- a/packages/frontend/src/pages/share.vue +++ b/packages/frontend/src/pages/share.vue @@ -37,6 +37,7 @@ import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkPostForm from '@/components/MkPostForm.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { postMessageToParentWindow } from '@/scripts/post-message.js'; import { i18n } from '@/i18n.js'; @@ -76,7 +77,7 @@ async function init() { ] // TypeScriptの指示通りに変換する .map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q) - .map(q => os.api('users/show', q) + .map(q => misskeyApi('users/show', q) .then(user => { visibleUsers.value.push(user); }, () => { @@ -91,11 +92,11 @@ async function init() { const replyId = urlParams.get('replyId'); const replyUri = urlParams.get('replyUri'); if (replyId) { - reply.value = await os.api('notes/show', { + reply.value = await misskeyApi('notes/show', { noteId: replyId, }); } else if (replyUri) { - const obj = await os.api('ap/show', { + const obj = await misskeyApi('ap/show', { uri: replyUri, }); if (obj.type === 'Note') { @@ -108,11 +109,11 @@ async function init() { const renoteId = urlParams.get('renoteId'); const renoteUri = urlParams.get('renoteUri'); if (renoteId) { - renote.value = await os.api('notes/show', { + renote.value = await misskeyApi('notes/show', { noteId: renoteId, }); } else if (renoteUri) { - const obj = await os.api('ap/show', { + const obj = await misskeyApi('ap/show', { uri: renoteUri, }); if (obj.type === 'Note') { @@ -126,7 +127,7 @@ async function init() { if (fileIds) { await Promise.all( fileIds.split(',') - .map(fileId => os.api('drive/files/show', { fileId }) + .map(fileId => misskeyApi('drive/files/show', { fileId }) .then(file => { files.value.push(file); }, () => { diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue index 4009652bcf..0c2ad9102d 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -31,6 +31,7 @@ import MkAnimBg from '@/components/MkAnimBg.vue'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const submitting = ref(false); @@ -42,7 +43,7 @@ function submit() { if (submitting.value) return; submitting.value = true; - os.api('signup-pending', { + misskeyApi('signup-pending', { code: props.code, }).then(res => { if (res.pendingApproval) { diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index f5cefeddb4..f92cc3dfcd 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -41,6 +41,7 @@ import MkInfo from '@/components/MkInfo.vue'; import MkPostForm from '@/components/MkPostForm.vue'; import { scroll } from '@/scripts/scroll.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; @@ -125,7 +126,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> { } async function chooseChannel(ev: MouseEvent): Promise<void> { - const channels = await os.api('channels/my-favorites', { + const channels = await misskeyApi('channels/my-favorites', { limit: 100, }); const items: MenuItem[] = [ diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index 3ec23df7b8..fcff94e10f 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -28,10 +28,10 @@ import { computed, watch, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkTimeline from '@/components/MkTimeline.vue'; import { scroll } from '@/scripts/scroll.js'; -import * as os from '@/os.js'; -import { useRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -45,7 +45,7 @@ const tlEl = shallowRef<InstanceType<typeof MkTimeline>>(); const rootEl = shallowRef<HTMLElement>(); watch(() => props.listId, async () => { - list.value = await os.api('users/lists/show', { + list.value = await misskeyApi('users/lists/show', { listId: props.listId, }); }, { immediate: true }); diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue index bd1159cb32..798b640647 100644 --- a/packages/frontend/src/pages/user/activity.following.vue +++ b/packages/frontend/src/pages/user/activity.following.vue @@ -18,7 +18,7 @@ import { onMounted, shallowRef, ref } from 'vue'; import { Chart, ChartDataset } from 'chart.js'; import * as Misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; @@ -61,7 +61,7 @@ async function renderChart() { })); }; - const raw = await os.api('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' }); const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; diff --git a/packages/frontend/src/pages/user/activity.heatmap.vue b/packages/frontend/src/pages/user/activity.heatmap.vue index ff46db9653..ea3276a890 100644 --- a/packages/frontend/src/pages/user/activity.heatmap.vue +++ b/packages/frontend/src/pages/user/activity.heatmap.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, nextTick, watch, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import * as Misskey from 'misskey-js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { alpha } from '@/scripts/color.js'; @@ -74,7 +74,7 @@ async function renderChart() { let values; if (props.src === 'notes') { - const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); values = raw.inc; } diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue index dd035641d8..a55d98d989 100644 --- a/packages/frontend/src/pages/user/activity.notes.vue +++ b/packages/frontend/src/pages/user/activity.notes.vue @@ -18,7 +18,7 @@ import { onMounted, shallowRef, ref } from 'vue'; import { Chart, ChartDataset } from 'chart.js'; import * as Misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; @@ -61,7 +61,7 @@ async function renderChart() { })); }; - const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue index 2dd9a1570f..fe9acd322c 100644 --- a/packages/frontend/src/pages/user/activity.pv.vue +++ b/packages/frontend/src/pages/user/activity.pv.vue @@ -18,7 +18,7 @@ import { onMounted, shallowRef, ref } from 'vue'; import { Chart, ChartDataset } from 'chart.js'; import * as Misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; @@ -61,7 +61,7 @@ async function renderChart() { })); }; - const raw = await os.api('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' }); const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; diff --git a/packages/frontend/src/pages/user/followers.vue b/packages/frontend/src/pages/user/followers.vue index 36f1b4543e..4a898f1ee7 100644 --- a/packages/frontend/src/pages/user/followers.vue +++ b/packages/frontend/src/pages/user/followers.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XFollowList from './follow-list.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; @@ -37,7 +37,7 @@ const error = ref<any>(null); function fetchUser(): void { if (props.acct == null) return; user.value = null; - os.api('users/show', Misskey.acct.parse(props.acct)).then(u => { + misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => { user.value = u; }).catch(err => { error.value = err; diff --git a/packages/frontend/src/pages/user/following.vue b/packages/frontend/src/pages/user/following.vue index 43876b77c0..bfa962f259 100644 --- a/packages/frontend/src/pages/user/following.vue +++ b/packages/frontend/src/pages/user/following.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XFollowList from './follow-list.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; @@ -37,7 +37,7 @@ const error = ref<any>(null); function fetchUser(): void { if (props.acct == null) return; user.value = null; - os.api('users/show', Misskey.acct.parse(props.acct)).then(u => { + misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => { user.value = u; }).catch(err => { error.value = err; diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 9de42e2f37..ed7dabce6b 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -185,14 +185,14 @@ import { getUserMenu } from '@/scripts/get-user-menu.js'; import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; import * as os from '@/os.js'; -import { useRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { $i, iAmModerator } from '@/account.js'; import { dateString } from '@/filters/date.js'; import { confetti } from '@/scripts/confetti.js'; -import { api } from '@/os.js'; import { defaultStore } from '@/store.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; +import { useRouter } from '@/global/router/supplier.js'; function calcAge(birthdate: string): number { const date = new Date(birthdate); @@ -262,7 +262,7 @@ const background = computed(() => { }); watch(moderationNote, async () => { - await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote.value }); + await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value }); }); const pagination = { @@ -333,7 +333,7 @@ function adjustMemoTextarea() { } async function updateMemo() { - await api('users/update-memo', { + await misskeyApi('users/update-memo', { memo: memoDraft.value, userId: props.user.id, }); diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue index 30817db77c..315999ee84 100644 --- a/packages/frontend/src/pages/user/index.files.vue +++ b/packages/frontend/src/pages/user/index.files.vue @@ -37,7 +37,7 @@ import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { notePage } from '@/filters/note.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import { defaultStore } from '@/store.js'; @@ -61,7 +61,7 @@ function thumbnail(image: Misskey.entities.DriveFile): string { } onMounted(() => { - os.api('users/notes', { + misskeyApi('users/notes', { userId: props.user.id, withFiles: true, limit: 15, diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index 44b4f84ca3..dea5fb7da3 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { acct as getAcct } from '@/filters/user.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; @@ -63,7 +63,7 @@ const error = ref<any>(null); function fetchUser(): void { if (props.acct == null) return; user.value = null; - os.api('users/show', Misskey.acct.parse(props.acct)).then(u => { + misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => { user.value = u; }).catch(err => { error.value = err; diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue index 50f86a0ae2..f4c86da6a7 100644 --- a/packages/frontend/src/pages/welcome.entrance.a.vue +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -39,7 +39,7 @@ import XTimeline from './welcome.timeline.vue'; import MarqueeText from '@/components/MkMarquee.vue'; import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue'; import misskeysvg from '/client-assets/sharkey.svg'; -import * as os from '@/os.js'; +import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue'; import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; @@ -53,11 +53,11 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string return getProxiedImageUrl(instance.iconUrl, 'preview'); } -os.api('meta', { detail: true }).then(_meta => { +misskeyApi('meta', { detail: true }).then(_meta => { meta.value = _meta; }); -os.apiGet('federation/instances', { +misskeyApiGet('federation/instances', { sort: '+pubSub', limit: 20, }).then(_instances => { diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index c2f9d4e585..9a27198977 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -40,6 +40,7 @@ import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import { host, version } from '@/config.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; import MkAnimBg from '@/components/MkAnimBg.vue'; @@ -52,7 +53,7 @@ function submit() { if (submitting.value) return; submitting.value = true; - os.api('admin/accounts/create', { + misskeyApi('admin/accounts/create', { username: username.value, password: password.value, }).then(res => { diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index 2cbe0ed9b1..228f5a13b7 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -32,14 +32,14 @@ import { onUpdated, ref, shallowRef } from 'vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; import MkMediaList from '@/components/MkMediaList.vue'; import MkPoll from '@/components/MkPoll.vue'; -import * as os from '@/os.js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { getScrollContainer } from '@/scripts/scroll.js'; const notes = ref<Misskey.entities.Note[]>([]); const isScrolling = ref(false); const scrollEl = shallowRef<HTMLElement>(); -os.apiGet('notes/featured').then(_notes => { +misskeyApiGet('notes/featured').then(_notes => { notes.value = _notes; }); diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue index 7f0af1b83e..225ab91514 100644 --- a/packages/frontend/src/pages/welcome.vue +++ b/packages/frontend/src/pages/welcome.vue @@ -16,12 +16,12 @@ import * as Misskey from 'misskey-js'; import XSetup from './welcome.setup.vue'; import XEntrance from './welcome.entrance.a.vue'; import { instanceName } from '@/config.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; const meta = ref<Misskey.entities.MetaResponse | null>(null); -os.api('meta', { detail: true }).then(res => { +misskeyApi('meta', { detail: true }).then(res => { meta.value = res; }); diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts index b2254a0611..8723110b08 100644 --- a/packages/frontend/src/pizzax.ts +++ b/packages/frontend/src/pizzax.ts @@ -8,7 +8,7 @@ import { onUnmounted, Ref, ref, watch } from 'vue'; import { BroadcastChannel } from 'broadcast-channel'; import { $i } from '@/account.js'; -import { api } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { get, set } from '@/scripts/idb-proxy.js'; import { defaultStore } from '@/store.js'; import { useStream } from '@/stream.js'; @@ -134,7 +134,7 @@ export class Storage<T extends StateDef> { window.setTimeout(async () => { await defaultStore.ready; - api('i/registry/get-all', { scope: ['client', this.key] }) + misskeyApi('i/registry/get-all', { scope: ['client', this.key] }) .then(kvs => { const cache: Partial<T> = {}; for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) { @@ -168,7 +168,7 @@ export class Storage<T extends StateDef> { this.reactiveState[key].value = this.state[key] = rawValue; return this.addIdbSetJob(async () => { - if (_DEV_) console.log(`set ${key} start`); + if (_DEV_) console.log(`set ${String(key)} start`); switch (this.def[key].where) { case 'device': { this.pizzaxChannel.postMessage({ @@ -199,7 +199,7 @@ export class Storage<T extends StateDef> { const cache = await get(this.registryCacheKeyName) || {}; cache[key] = rawValue; await set(this.registryCacheKeyName, cache); - await api('i/registry/set', { + await misskeyApi('i/registry/set', { scope: ['client', this.key], key: key.toString(), value: rawValue, @@ -207,7 +207,7 @@ export class Storage<T extends StateDef> { break; } } - if (_DEV_) console.log(`set ${key} complete`); + if (_DEV_) console.log(`set ${String(key)} complete`); }); } @@ -225,7 +225,10 @@ export class Storage<T extends StateDef> { * 特定のキーの、簡易的なgetter/setterを作ります * 主にvue場で設定コントロールのmodelとして使う用 */ - public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]) { + public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): { + get: () => T[K]['default']; + set: (value: T[K]['default']) => void; + } { const valueRef = ref(this.state[key]); const stop = watch(this.reactiveState[key], val => { diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index 5e49af4858..acc3e836fb 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -4,7 +4,7 @@ */ import { Interpreter, Parser, utils, values } from '@syuilo/aiscript'; -import { createAiScriptEnv } from '@/scripts/aiscript/api.js'; +import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; import { inputText } from '@/os.js'; import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js'; @@ -19,19 +19,7 @@ export async function install(plugin: Plugin): Promise<void> { plugin: plugin, storageKey: 'plugins:' + plugin.id, }), { - in: (q): Promise<string> => { - return new Promise(ok => { - inputText({ - title: q, - }).then(({ canceled, result: a }) => { - if (canceled) { - ok(''); - } else { - ok(a); - } - }); - }); - }, + in: aiScriptReadline, out: (value): void => { console.log(value); }, diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts deleted file mode 100644 index f12f7bde79..0000000000 --- a/packages/frontend/src/router.ts +++ /dev/null @@ -1,594 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { AsyncComponentLoader, defineAsyncComponent, inject } from 'vue'; -import { Router } from '@/nirax.js'; -import { $i, iAmModerator } from '@/account.js'; -import MkLoading from '@/pages/_loading_.vue'; -import MkError from '@/pages/_error_.vue'; - -export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ - loader: loader, - loadingComponent: MkLoading, - errorComponent: MkError, -}); - -export const routes = [{ - path: '/@:initUser/pages/:initPageName/view-source', - component: page(() => import('./pages/page-editor/page-editor.vue')), -}, { - path: '/@:username/pages/:pageName', - component: page(() => import('./pages/page.vue')), -}, { - path: '/@:acct/following', - component: page(() => import('./pages/user/following.vue')), -}, { - path: '/@:acct/followers', - component: page(() => import('./pages/user/followers.vue')), -}, { - name: 'user', - path: '/@:acct/:page?', - component: page(() => import('./pages/user/index.vue')), -}, { - name: 'note', - path: '/notes/:noteId', - component: page(() => import('./pages/note.vue')), -}, { - name: 'list', - path: '/list/:listId', - component: page(() => import('./pages/list.vue')), -}, { - path: '/clips/:clipId', - component: page(() => import('./pages/clip.vue')), -}, { - path: '/instance-info/:host', - component: page(() => import('./pages/instance-info.vue')), -}, { - name: 'settings', - path: '/settings', - component: page(() => import('./pages/settings/index.vue')), - loginRequired: true, - children: [{ - path: '/profile', - name: 'profile', - component: page(() => import('./pages/settings/profile.vue')), - }, { - path: '/avatar-decoration', - name: 'avatarDecoration', - component: page(() => import('./pages/settings/avatar-decoration.vue')), - }, { - path: '/roles', - name: 'roles', - component: page(() => import('./pages/settings/roles.vue')), - }, { - path: '/privacy', - name: 'privacy', - component: page(() => import('./pages/settings/privacy.vue')), - }, { - path: '/emoji-picker', - name: 'emojiPicker', - component: page(() => import('./pages/settings/emoji-picker.vue')), - }, { - path: '/drive', - name: 'drive', - component: page(() => import('./pages/settings/drive.vue')), - }, { - path: '/drive/cleaner', - name: 'drive', - component: page(() => import('./pages/settings/drive-cleaner.vue')), - }, { - path: '/notifications', - name: 'notifications', - component: page(() => import('./pages/settings/notifications.vue')), - }, { - path: '/email', - name: 'email', - component: page(() => import('./pages/settings/email.vue')), - }, { - path: '/security', - name: 'security', - component: page(() => import('./pages/settings/security.vue')), - }, { - path: '/general', - name: 'general', - component: page(() => import('./pages/settings/general.vue')), - }, { - path: '/theme/install', - name: 'theme', - component: page(() => import('./pages/settings/theme.install.vue')), - }, { - path: '/theme/manage', - name: 'theme', - component: page(() => import('./pages/settings/theme.manage.vue')), - }, { - path: '/theme', - name: 'theme', - component: page(() => import('./pages/settings/theme.vue')), - }, { - path: '/navbar', - name: 'navbar', - component: page(() => import('./pages/settings/navbar.vue')), - }, { - path: '/statusbar', - name: 'statusbar', - component: page(() => import('./pages/settings/statusbar.vue')), - }, { - path: '/sounds', - name: 'sounds', - component: page(() => import('./pages/settings/sounds.vue')), - }, { - path: '/plugin/install', - name: 'plugin', - component: page(() => import('./pages/settings/plugin.install.vue')), - }, { - path: '/plugin', - name: 'plugin', - component: page(() => import('./pages/settings/plugin.vue')), - }, { - path: '/import-export', - name: 'import-export', - component: page(() => import('./pages/settings/import-export.vue')), - }, { - path: '/mute-block', - name: 'mute-block', - component: page(() => import('./pages/settings/mute-block.vue')), - }, { - path: '/api', - name: 'api', - component: page(() => import('./pages/settings/api.vue')), - }, { - path: '/apps', - name: 'api', - component: page(() => import('./pages/settings/apps.vue')), - }, { - path: '/webhook/edit/:webhookId', - name: 'webhook', - component: page(() => import('./pages/settings/webhook.edit.vue')), - }, { - path: '/webhook/new', - name: 'webhook', - component: page(() => import('./pages/settings/webhook.new.vue')), - }, { - path: '/webhook', - name: 'webhook', - component: page(() => import('./pages/settings/webhook.vue')), - }, { - path: '/deck', - name: 'deck', - component: page(() => import('./pages/settings/deck.vue')), - }, { - path: '/preferences-backups', - name: 'preferences-backups', - component: page(() => import('./pages/settings/preferences-backups.vue')), - }, { - path: '/migration', - name: 'migration', - component: page(() => import('./pages/settings/migration.vue')), - }, { - path: '/custom-css', - name: 'general', - component: page(() => import('./pages/settings/custom-css.vue')), - }, { - path: '/accounts', - name: 'profile', - component: page(() => import('./pages/settings/accounts.vue')), - }, { - path: '/other', - name: 'other', - component: page(() => import('./pages/settings/other.vue')), - }, { - path: '/', - component: page(() => import('./pages/_empty_.vue')), - }], -}, { - path: '/reset-password/:token?', - component: page(() => import('./pages/reset-password.vue')), -}, { - path: '/signup-complete/:code', - component: page(() => import('./pages/signup-complete.vue')), -}, { - path: '/announcements', - component: page(() => import('./pages/announcements.vue')), -}, { - path: '/about', - component: page(() => import('./pages/about.vue')), - hash: 'initialTab', -}, { - path: '/about-sharkey', - component: page(() => import('./pages/about-sharkey.vue')), -}, { - path: '/invite', - name: 'invite', - component: page(() => import('./pages/invite.vue')), -}, { - path: '/ads', - component: page(() => import('./pages/ads.vue')), -}, { - path: '/theme-editor', - component: page(() => import('./pages/theme-editor.vue')), - loginRequired: true, -}, { - path: '/roles/:role', - component: page(() => import('./pages/role.vue')), -}, { - path: '/user-tags/:tag', - component: page(() => import('./pages/user-tag.vue')), -}, { - path: '/explore', - component: page(() => import('./pages/explore.vue')), - hash: 'initialTab', -}, { - path: '/search', - component: page(() => import('./pages/search.vue')), - query: { - q: 'query', - channel: 'channel', - type: 'type', - origin: 'origin', - }, -}, { - path: '/authorize-follow', - component: page(() => import('./pages/follow.vue')), - loginRequired: true, -}, { - path: '/share', - component: page(() => import('./pages/share.vue')), - loginRequired: true, -}, { - path: '/api-console', - component: page(() => import('./pages/api-console.vue')), - loginRequired: true, -}, { - path: '/scratchpad', - component: page(() => import('./pages/scratchpad.vue')), -}, { - path: '/auth/:token', - component: page(() => import('./pages/auth.vue')), -}, { - path: '/miauth/:session', - component: page(() => import('./pages/miauth.vue')), - query: { - callback: 'callback', - name: 'name', - icon: 'icon', - permission: 'permission', - }, -}, { - path: '/tags/:tag', - component: page(() => import('./pages/tag.vue')), -}, { - path: '/pages/new', - component: page(() => import('./pages/page-editor/page-editor.vue')), - loginRequired: true, -}, { - path: '/pages/edit/:initPageId', - component: page(() => import('./pages/page-editor/page-editor.vue')), - loginRequired: true, -}, { - path: '/pages', - component: page(() => import('./pages/pages.vue')), -}, { - path: '/play/:id/edit', - component: page(() => import('./pages/flash/flash-edit.vue')), - loginRequired: true, -}, { - path: '/play/new', - component: page(() => import('./pages/flash/flash-edit.vue')), - loginRequired: true, -}, { - path: '/play/:id', - component: page(() => import('./pages/flash/flash.vue')), -}, { - path: '/play', - component: page(() => import('./pages/flash/flash-index.vue')), -}, { - path: '/gallery/:postId/edit', - component: page(() => import('./pages/gallery/edit.vue')), - loginRequired: true, -}, { - path: '/gallery/new', - component: page(() => import('./pages/gallery/edit.vue')), - loginRequired: true, -}, { - path: '/gallery/:postId', - component: page(() => import('./pages/gallery/post.vue')), -}, { - path: '/gallery', - component: page(() => import('./pages/gallery/index.vue')), -}, { - path: '/channels/:channelId/edit', - component: page(() => import('./pages/channel-editor.vue')), - loginRequired: true, -}, { - path: '/channels/new', - component: page(() => import('./pages/channel-editor.vue')), - loginRequired: true, -}, { - path: '/channels/:channelId', - component: page(() => import('./pages/channel.vue')), -}, { - path: '/channels', - component: page(() => import('./pages/channels.vue')), -}, { - path: '/avatar-decorations', - name: 'avatarDecorations', - component: page(() => import('./pages/avatar-decorations.vue')), -}, { - path: '/custom-emojis-manager', - component: page(() => import('./pages/custom-emojis-manager.vue')), -}, { - path: '/registry/keys/:domain/:path(*)?', - component: page(() => import('./pages/registry.keys.vue')), -}, { - path: '/registry/value/:domain/:path(*)?', - component: page(() => import('./pages/registry.value.vue')), -}, { - path: '/registry', - component: page(() => import('./pages/registry.vue')), -}, { - path: '/install-extentions', - component: page(() => import('./pages/install-extentions.vue')), - loginRequired: true, -}, { - path: '/admin/user/:userId', - component: iAmModerator ? page(() => import('./pages/admin-user.vue')) : page(() => import('./pages/not-found.vue')), -}, { - path: '/admin/file/:fileId', - component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')), -}, { - path: '/admin', - component: iAmModerator ? page(() => import('./pages/admin/index.vue')) : page(() => import('./pages/not-found.vue')), - children: [{ - path: '/overview', - name: 'overview', - component: page(() => import('./pages/admin/overview.vue')), - }, { - path: '/users', - name: 'users', - component: page(() => import('./pages/admin/users.vue')), - }, { - path: '/emojis', - name: 'emojis', - component: page(() => import('./pages/custom-emojis-manager.vue')), - }, { - path: '/avatar-decorations', - name: 'avatarDecorations', - component: page(() => import('./pages/avatar-decorations.vue')), - }, { - path: '/queue', - name: 'queue', - component: page(() => import('./pages/admin/queue.vue')), - }, { - path: '/files', - name: 'files', - component: page(() => import('./pages/admin/files.vue')), - }, { - path: '/federation', - name: 'federation', - component: page(() => import('./pages/admin/federation.vue')), - }, { - path: '/announcements', - name: 'announcements', - component: page(() => import('./pages/admin/announcements.vue')), - }, { - path: '/ads', - name: 'ads', - component: page(() => import('./pages/admin/ads.vue')), - }, { - path: '/roles/:id/edit', - name: 'roles', - component: page(() => import('./pages/admin/roles.edit.vue')), - }, { - path: '/roles/new', - name: 'roles', - component: page(() => import('./pages/admin/roles.edit.vue')), - }, { - path: '/roles/:id', - name: 'roles', - component: page(() => import('./pages/admin/roles.role.vue')), - }, { - path: '/roles', - name: 'roles', - component: page(() => import('./pages/admin/roles.vue')), - }, { - path: '/database', - name: 'database', - component: page(() => import('./pages/admin/database.vue')), - }, { - path: '/abuses', - name: 'abuses', - component: page(() => import('./pages/admin/abuses.vue')), - }, { - path: '/modlog', - name: 'modlog', - component: page(() => import('./pages/admin/modlog.vue')), - }, { - path: '/settings', - name: 'settings', - component: page(() => import('./pages/admin/settings.vue')), - }, { - path: '/branding', - name: 'branding', - component: page(() => import('./pages/admin/branding.vue')), - }, { - path: '/moderation', - name: 'moderation', - component: page(() => import('./pages/admin/moderation.vue')), - }, { - path: '/email-settings', - name: 'email-settings', - component: page(() => import('./pages/admin/email-settings.vue')), - }, { - path: '/object-storage', - name: 'object-storage', - component: page(() => import('./pages/admin/object-storage.vue')), - }, { - path: '/security', - name: 'security', - component: page(() => import('./pages/admin/security.vue')), - }, { - path: '/relays', - name: 'relays', - component: page(() => import('./pages/admin/relays.vue')), - }, { - path: '/instance-block', - name: 'instance-block', - component: page(() => import('./pages/admin/instance-block.vue')), - }, { - path: '/proxy-account', - name: 'proxy-account', - component: page(() => import('./pages/admin/proxy-account.vue')), - }, { - path: '/external-services', - name: 'external-services', - component: page(() => import('./pages/admin/external-services.vue')), - }, { - path: '/other-settings', - name: 'other-settings', - component: page(() => import('./pages/admin/other-settings.vue')), - }, { - path: '/server-rules', - name: 'server-rules', - component: page(() => import('./pages/admin/server-rules.vue')), - }, { - path: '/invites', - name: 'invites', - component: page(() => import('./pages/admin/invites.vue')), - }, { - path: '/approvals', - name: 'approvals', - component: page(() => import('./pages/admin/approvals.vue')), - }, { - path: '/', - component: page(() => import('./pages/_empty_.vue')), - }], -}, { - path: '/my/notifications', - component: page(() => import('./pages/notifications.vue')), - loginRequired: true, -}, { - path: '/my/favorites', - component: page(() => import('./pages/favorites.vue')), - loginRequired: true, -}, { - path: '/my/achievements', - component: page(() => import('./pages/achievements.vue')), - loginRequired: true, -}, { - path: '/my/drive/folder/:folder', - component: page(() => import('./pages/drive.vue')), - loginRequired: true, -}, { - path: '/my/drive', - component: page(() => import('./pages/drive.vue')), - loginRequired: true, -}, { - path: '/my/drive/file/:fileId', - component: page(() => import('./pages/drive.file.vue')), - loginRequired: true, -}, { - path: '/my/follow-requests', - component: page(() => import('./pages/follow-requests.vue')), - loginRequired: true, -}, { - path: '/my/lists/:listId', - component: page(() => import('./pages/my-lists/list.vue')), - loginRequired: true, -}, { - path: '/my/lists', - component: page(() => import('./pages/my-lists/index.vue')), - loginRequired: true, -}, { - path: '/my/clips', - component: page(() => import('./pages/my-clips/index.vue')), - loginRequired: true, -}, { - path: '/my/antennas/create', - component: page(() => import('./pages/my-antennas/create.vue')), - loginRequired: true, -}, { - path: '/my/antennas/:antennaId', - component: page(() => import('./pages/my-antennas/edit.vue')), - loginRequired: true, -}, { - path: '/my/antennas', - component: page(() => import('./pages/my-antennas/index.vue')), - loginRequired: true, -}, { - path: '/timeline/list/:listId', - component: page(() => import('./pages/user-list-timeline.vue')), - loginRequired: true, -}, { - path: '/timeline/antenna/:antennaId', - component: page(() => import('./pages/antenna-timeline.vue')), - loginRequired: true, -}, { - path: '/clicker', - component: page(() => import('./pages/clicker.vue')), - loginRequired: true, -}, { - path: '/timeline', - component: page(() => import('./pages/timeline.vue')), -}, { - name: 'index', - path: '/', - component: $i ? page(() => import('./pages/timeline.vue')) : page(() => import('./pages/welcome.vue')), - globalCacheKey: 'index', -}, { - path: '/:(*)', - component: page(() => import('./pages/not-found.vue')), -}]; - -export const mainRouter = new Router(routes, location.pathname + location.search + location.hash, !!$i, page(() => import('@/pages/not-found.vue'))); - -window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href); - -const scrollPosStore = new Map<string, number>(); -let restoring = false; - -window.setInterval(() => { - if (!restoring) { - scrollPosStore.set(window.history.state?.key, window.scrollY); - } -}, 1000); - -mainRouter.addListener('push', ctx => { - window.history.pushState({ key: ctx.key }, '', ctx.path); - - restoring = true; - const scrollPos = scrollPosStore.get(ctx.key) ?? 0; - window.scroll({ top: scrollPos, behavior: 'instant' }); - - if (scrollPos !== 0) { - window.setTimeout(() => { - // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール - window.scroll({ top: scrollPos, behavior: 'instant' }); - }, 100); - restoring = false; - } else { - restoring = false; - } -}); - -mainRouter.addListener('same', () => { - window.scroll({ top: 0, behavior: 'smooth' }); -}); - -window.addEventListener('popstate', (event) => { - mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); - - restoring = true; - const scrollPos = scrollPosStore.get(event.state?.key) ?? 0; - window.scroll({ top: scrollPos, behavior: 'instant' }); - window.setTimeout(() => { - // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール - window.scroll({ top: scrollPos, behavior: 'instant' }); - restoring = false; - }, 100); -}); - -export function useRouter(): Router { - return inject<Router | null>('router', null) ?? mainRouter; -} diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts index e7585fcf81..67d997f09b 100644 --- a/packages/frontend/src/scripts/achievements.ts +++ b/packages/frontend/src/scripts/achievements.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; export const ACHIEVEMENT_TYPES = [ @@ -83,6 +83,8 @@ export const ACHIEVEMENT_TYPES = [ 'brainDiver', 'smashTestNotificationButton', 'tutorialCompleted', + 'bubbleGameExplodingHead', + 'bubbleGameDoubleExplodingHead', ] as const; export const ACHIEVEMENT_BADGES = { @@ -466,6 +468,16 @@ export const ACHIEVEMENT_BADGES = { bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', frame: 'bronze', }, + 'bubbleGameExplodingHead': { + img: '/fluent-emoji/1f92f.png', + bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', + frame: 'bronze', + }, + 'bubbleGameDoubleExplodingHead': { + img: '/fluent-emoji/1f92f.png', + bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', + frame: 'silver', + }, /* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107> } as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], { img: string; @@ -489,7 +501,7 @@ export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) { window.setTimeout(() => { claimingQueue.delete(type); }, 500); - os.api('i/claim-achievement', { name: type }); + misskeyApi('i/claim-achievement', { name: type }); } if (_DEV_) { diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 038ae23109..c13849cc8f 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -5,12 +5,23 @@ import { utils, values } from '@syuilo/aiscript'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; import { url, lang } from '@/config.js'; import { nyaize } from '@/scripts/nyaize.js'; +export function aiScriptReadline(q: string): Promise<string> { + return new Promise(ok => { + os.inputText({ + title: q, + }).then(({ result: a }) => { + ok(a ?? ''); + }); + }); +} + export function createAiScriptEnv(opts) { return { USER_ID: $i ? values.STR($i.id) : values.NULL, @@ -44,7 +55,7 @@ export function createAiScriptEnv(opts) { if (typeof token.value !== 'string') throw new Error('invalid token'); } const actualToken: string|null = token?.value ?? opts.token ?? null; - return os.api(ep.value, utils.valToJs(param), actualToken).then(res => { + return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => { return utils.jsToVal(res); }, err => { return values.ERROR('request_failed', utils.jsToVal(err)); diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index 08ba1e6d9b..215ac4cc69 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -218,7 +218,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't }; } -function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'type'> { +function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiMfm, 'id' | 'type'> { utils.assertObject(def); const text = def.value.get('text'); @@ -241,7 +241,7 @@ function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'typ color: color?.value, font: font?.value, onClickEv: (evId: string) => { - if (onClickEv) call(onClickEv, values.STR(evId)); + if (onClickEv) call(onClickEv, [values.STR(evId)]); }, }; } diff --git a/packages/frontend/src/scripts/clicker-game.ts b/packages/frontend/src/scripts/clicker-game.ts index 5ad076e5ef..360bea903c 100644 --- a/packages/frontend/src/scripts/clicker-game.ts +++ b/packages/frontend/src/scripts/clicker-game.ts @@ -4,7 +4,7 @@ */ import { ref, computed } from 'vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; type SaveData = { gameVersion: number; @@ -23,7 +23,7 @@ let prev = ''; export async function load() { try { - saveData.value = await os.api('i/registry/get', { + saveData.value = await misskeyApi('i/registry/get', { scope: ['clickerGame'], key: 'saveData', }); @@ -63,7 +63,7 @@ export async function save() { const current = JSON.stringify(saveData.value); if (current === prev) return; - await os.api('i/registry/set', { + await misskeyApi('i/registry/set', { scope: ['clickerGame'], key: 'saveData', value: saveData.value, diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts new file mode 100644 index 0000000000..b6e735ddf2 --- /dev/null +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -0,0 +1,400 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EventEmitter } from 'eventemitter3'; +import * as Matter from 'matter-js'; +import * as sound from '@/scripts/sound.js'; + +export type Mono = { + id: string; + level: number; + size: number; + shape: 'circle' | 'rectangle'; + score: number; + dropCandidate: boolean; + sfxPitch: number; + img: string; + imgSize: number; + spriteScale: number; +}; + +const PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる + +export class DropAndFusionGame extends EventEmitter<{ + changeScore: (newScore: number) => void; + changeCombo: (newCombo: number) => void; + changeStock: (newStock: { id: string; mono: Mono }[]) => void; + dropped: () => void; + fusioned: (x: number, y: number, scoreDelta: number) => void; + monoAdded: (mono: Mono) => void; + gameOver: () => void; +}> { + private COMBO_INTERVAL = 1000; + public readonly DROP_INTERVAL = 500; + public readonly PLAYAREA_MARGIN = 25; + private STOCK_MAX = 4; + private loaded = false; + private engine: Matter.Engine; + private render: Matter.Render; + private runner: Matter.Runner; + private overflowCollider: Matter.Body; + private isGameOver = false; + + private gameWidth: number; + private gameHeight: number; + private monoDefinitions: Mono[] = []; + private monoTextures: Record<string, Blob> = {}; + private monoTextureUrls: Record<string, string> = {}; + + /** + * フィールドに出ていて、かつ合体の対象となるアイテム + */ + private activeBodyIds: Matter.Body['id'][] = []; + + private latestDroppedBodyId: Matter.Body['id'] | null = null; + + private latestDroppedAt = 0; + private latestFusionedAt = 0; + private stock: { id: string; mono: Mono }[] = []; + + private _combo = 0; + private get combo() { + return this._combo; + } + private set combo(value: number) { + this._combo = value; + this.emit('changeCombo', value); + } + + private _score = 0; + private get score() { + return this._score; + } + private set score(value: number) { + this._score = value; + this.emit('changeScore', value); + } + + private comboIntervalId: number | null = null; + + constructor(opts: { + canvas: HTMLCanvasElement; + width: number; + height: number; + monoDefinitions: Mono[]; + }) { + super(); + + this.gameWidth = opts.width; + this.gameHeight = opts.height; + this.monoDefinitions = opts.monoDefinitions; + + this.engine = Matter.Engine.create({ + constraintIterations: 2 * PHYSICS_QUALITY_FACTOR, + positionIterations: 6 * PHYSICS_QUALITY_FACTOR, + velocityIterations: 4 * PHYSICS_QUALITY_FACTOR, + gravity: { + x: 0, + y: 1, + }, + timing: { + timeScale: 2, + }, + enableSleeping: false, + }); + + this.render = Matter.Render.create({ + engine: this.engine, + canvas: opts.canvas, + options: { + width: this.gameWidth, + height: this.gameHeight, + background: 'transparent', // transparent to hide + wireframeBackground: 'transparent', // transparent to hide + wireframes: false, + showSleeping: false, + pixelRatio: Math.max(2, window.devicePixelRatio), + }, + }); + + Matter.Render.run(this.render); + + this.runner = Matter.Runner.create(); + Matter.Runner.run(this.runner, this.engine); + + this.engine.world.bodies = []; + + //#region walls + const WALL_OPTIONS: Matter.IChamferableBodyDefinition = { + isStatic: true, + friction: 0.7, + slop: 1.0, + render: { + strokeStyle: 'transparent', + fillStyle: 'transparent', + }, + }; + + const thickness = 100; + Matter.Composite.add(this.engine.world, [ + Matter.Bodies.rectangle(this.gameWidth / 2, this.gameHeight + (thickness / 2) - this.PLAYAREA_MARGIN, this.gameWidth, thickness, WALL_OPTIONS), + Matter.Bodies.rectangle(this.gameWidth + (thickness / 2) - this.PLAYAREA_MARGIN, this.gameHeight / 2, thickness, this.gameHeight, WALL_OPTIONS), + Matter.Bodies.rectangle(-((thickness / 2) - this.PLAYAREA_MARGIN), this.gameHeight / 2, thickness, this.gameHeight, WALL_OPTIONS), + ]); + //#endregion + + this.overflowCollider = Matter.Bodies.rectangle(this.gameWidth / 2, 0, this.gameWidth, 200, { + isStatic: true, + isSensor: true, + render: { + strokeStyle: 'transparent', + fillStyle: 'transparent', + }, + }); + Matter.Composite.add(this.engine.world, this.overflowCollider); + + // fit the render viewport to the scene + Matter.Render.lookAt(this.render, { + min: { x: 0, y: 0 }, + max: { x: this.gameWidth, y: this.gameHeight }, + }); + } + + private createBody(mono: Mono, x: number, y: number) { + const options: Matter.IBodyDefinition = { + label: mono.id, + //density: 0.0005, + density: mono.size / 1000, + restitution: 0.2, + frictionAir: 0.01, + friction: 0.7, + frictionStatic: 5, + slop: 1.0, + //mass: 0, + render: { + sprite: { + texture: mono.img, + xScale: (mono.size / mono.imgSize) * mono.spriteScale, + yScale: (mono.size / mono.imgSize) * mono.spriteScale, + }, + }, + }; + if (mono.shape === 'circle') { + return Matter.Bodies.circle(x, y, mono.size / 2, options); + } else if (mono.shape === 'rectangle') { + return Matter.Bodies.rectangle(x, y, mono.size, mono.size, options); + } else { + throw new Error('unrecognized shape'); + } + } + + private fusion(bodyA: Matter.Body, bodyB: Matter.Body) { + const now = Date.now(); + if (this.latestFusionedAt > now - this.COMBO_INTERVAL) { + this.combo++; + } else { + this.combo = 1; + } + this.latestFusionedAt = now; + + // TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する? + const newX = (bodyA.position.x + bodyB.position.x) / 2; + const newY = (bodyA.position.y + bodyB.position.y) / 2; + + Matter.Composite.remove(this.engine.world, [bodyA, bodyB]); + this.activeBodyIds = this.activeBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id); + + const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label)!; + const nextMono = this.monoDefinitions.find(x => x.level === currentMono.level + 1); + + if (nextMono) { + const body = this.createBody(nextMono, newX, newY); + Matter.Composite.add(this.engine.world, body); + + // 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする + window.setTimeout(() => { + this.activeBodyIds.push(body.id); + }, 100); + + const comboBonus = 1 + ((this.combo - 1) / 5); + const additionalScore = Math.round(currentMono.score * comboBonus); + this.score += additionalScore; + + // TODO: 効果音再生はコンポーネント側の責務なので移動する + const pan = ((newX / this.gameWidth) - 0.5) * 2; + sound.playUrl('/client-assets/drop-and-fusion/bubble2.mp3', 1, pan, nextMono.sfxPitch); + + this.emit('monoAdded', nextMono); + this.emit('fusioned', newX, newY, additionalScore); + } else { + //const VELOCITY = 30; + //for (let i = 0; i < 10; i++) { + // const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(Math.random() * 3)))!, x + ((Math.random() * VELOCITY) - (VELOCITY / 2)), y + ((Math.random() * VELOCITY) - (VELOCITY / 2))); + // Matter.Composite.add(world, body); + // bodies.push(body); + //} + //sound.playUrl({ + // type: 'syuilo/bubble2', + // volume: 1, + //}); + } + } + + private gameOver() { + this.isGameOver = true; + Matter.Runner.stop(this.runner); + this.emit('gameOver'); + } + + /** テクスチャをすべてキャッシュする */ + private async loadMonoTextures() { + async function loadSingleMonoTexture(mono: Mono, game: DropAndFusionGame) { + // Matter-js内にキャッシュがある場合はスキップ + if (game.render.textures[mono.img]) return; + console.log('loading', mono.img); + + let src = mono.img; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (game.monoTextureUrls[mono.img]) { + src = game.monoTextureUrls[mono.img]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + } else if (game.monoTextures[mono.img]) { + src = URL.createObjectURL(game.monoTextures[mono.img]); + game.monoTextureUrls[mono.img] = src; + } else { + const res = await fetch(mono.img); + const blob = await res.blob(); + game.monoTextures[mono.img] = blob; + src = URL.createObjectURL(blob); + game.monoTextureUrls[mono.img] = src; + } + + const image = new Image(); + image.src = src; + game.render.textures[mono.img] = image; + } + + return Promise.all(this.monoDefinitions.map(x => loadSingleMonoTexture(x, this))); + } + + public start() { + if (!this.loaded) throw new Error('game is not loaded yet'); + + for (let i = 0; i < this.STOCK_MAX; i++) { + this.stock.push({ + id: Math.random().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + }); + } + this.emit('changeStock', this.stock); + + // TODO: fusion予約状態のアイテムは光らせるなどの演出をすると楽しそう + let fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = []; + + const minCollisionEnergyForSound = 2.5; + const maxCollisionEnergyForSound = 9; + const soundPitchMax = 4; + const soundPitchMin = 0.5; + + Matter.Events.on(this.engine, 'collisionStart', (event) => { + for (const pairs of event.pairs) { + const { bodyA, bodyB } = pairs; + if (bodyA.id === this.overflowCollider.id || bodyB.id === this.overflowCollider.id) { + if (bodyA.id === this.latestDroppedBodyId || bodyB.id === this.latestDroppedBodyId) { + continue; + } + this.gameOver(); + break; + } + const shouldFusion = (bodyA.label === bodyB.label) && !fusionReservedPairs.some(x => x.bodyA.id === bodyA.id || x.bodyA.id === bodyB.id || x.bodyB.id === bodyA.id || x.bodyB.id === bodyB.id); + if (shouldFusion) { + if (this.activeBodyIds.includes(bodyA.id) && this.activeBodyIds.includes(bodyB.id)) { + this.fusion(bodyA, bodyB); + } else { + fusionReservedPairs.push({ bodyA, bodyB }); + window.setTimeout(() => { + fusionReservedPairs = fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id); + this.fusion(bodyA, bodyB); + }, 100); + } + } else { + const energy = pairs.collision.depth; + if (energy > minCollisionEnergyForSound) { + // TODO: 効果音再生はコンポーネント側の責務なので移動する + const vol = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4; + const pan = ((((bodyA.position.x + bodyB.position.x) / 2) / this.gameWidth) - 0.5) * 2; + const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10))); + sound.playUrl('/client-assets/drop-and-fusion/poi1.mp3', vol, pan, pitch); + } + } + } + }); + + this.comboIntervalId = window.setInterval(() => { + if (this.latestFusionedAt < Date.now() - this.COMBO_INTERVAL) { + this.combo = 0; + } + }, 500); + } + + public async load() { + await this.loadMonoTextures(); + this.loaded = true; + } + + public getTextureImageUrl(mono: Mono) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.monoTextureUrls[mono.img]) { + return this.monoTextureUrls[mono.img]; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + } else if (this.monoTextures[mono.img]) { + // Gameクラス内にキャッシュがある場合はそれを使う + const out = URL.createObjectURL(this.monoTextures[mono.img]); + this.monoTextureUrls[mono.img] = out; + return out; + } else { + return mono.img; + } + } + + public getActiveMonos() { + return this.engine.world.bodies.map(x => this.monoDefinitions.find((mono) => mono.id === x.label)!).filter(x => x !== undefined); + } + + public drop(_x: number) { + if (this.isGameOver) return; + if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) { + return; + } + const st = this.stock.shift()!; + this.stock.push({ + id: Math.random().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + }); + this.emit('changeStock', this.stock); + + const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (st.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (st.mono.size / 2), _x)); + const body = this.createBody(st.mono, x, 50 + st.mono.size / 2); + Matter.Composite.add(this.engine.world, body); + this.activeBodyIds.push(body.id); + this.latestDroppedBodyId = body.id; + this.latestDroppedAt = Date.now(); + this.emit('dropped'); + this.emit('monoAdded', st.mono); + + // TODO: 効果音再生はコンポーネント側の責務なので移動する + const pan = ((x / this.gameWidth) - 0.5) * 2; + sound.playUrl('/client-assets/drop-and-fusion/poi2.mp3', 1, pan); + } + + public dispose() { + if (this.comboIntervalId) window.clearInterval(this.comboIntervalId); + Matter.Render.stop(this.render); + Matter.Runner.stop(this.runner); + Matter.World.clear(this.engine.world, false); + Matter.Engine.clear(this.engine); + } +} diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts index 8885bf4b7f..4bd8bf94be 100644 --- a/packages/frontend/src/scripts/emojilist.ts +++ b/packages/frontend/src/scripts/emojilist.ts @@ -36,7 +36,8 @@ for (let i = 0; i < emojilist.length; i++) { export const emojiCharByCategory = _charGroupByCategory; export function getEmojiName(char: string): string | null { - const idx = _indexByChar.get(char); + // Colorize it because emojilist.json assumes that + const idx = _indexByChar.get(colorizeEmoji(char)); if (idx == null) { return null; } else { @@ -44,6 +45,10 @@ export function getEmojiName(char: string): string | null { } } +export function colorizeEmoji(char: string) { + return char.length === 1 ? `${char}\uFE0F` : char; +} + export interface CustomEmojiFolderTree { value: string; category: string; diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts index 222fd9b0b7..f7e0369419 100644 --- a/packages/frontend/src/scripts/form.ts +++ b/packages/frontend/src/scripts/form.ts @@ -3,7 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -type EnumItem = string | {label: string; value: string;}; +type EnumItem = string | { + label: string; + value: string; +}; + export type FormItem = { label?: string; type: 'string'; @@ -38,14 +42,21 @@ export type FormItem = { }[]; } | { label?: string; + type: 'range'; + default: number | null; + step: number; + min: number; + max: number; +} | { + label?: string; type: 'object'; default: Record<string, unknown> | null; - hidden: true; + hidden: boolean; } | { label?: string; type: 'array'; default: unknown[] | null; - hidden: true; + hidden: boolean; }; export type Form = Record<string, FormItem>; @@ -55,6 +66,7 @@ type GetItemType<Item extends FormItem> = Item['type'] extends 'number' ? number : Item['type'] extends 'boolean' ? boolean : Item['type'] extends 'radio' ? unknown : + Item['type'] extends 'range' ? number : Item['type'] extends 'enum' ? string : Item['type'] extends 'array' ? unknown[] : Item['type'] extends 'object' ? Record<string, unknown> diff --git a/packages/frontend/src/scripts/gen-search-query.ts b/packages/frontend/src/scripts/gen-search-query.ts index 54654980f2..068cd9cd93 100644 --- a/packages/frontend/src/scripts/gen-search-query.ts +++ b/packages/frontend/src/scripts/gen-search-query.ts @@ -18,7 +18,7 @@ export async function genSearchQuery(v: any, q: string) { host = at; } } else { - const user = await v.os.api('users/show', Misskey.acct.parse(at)).catch(x => null); + const user = await v.api('users/show', Misskey.acct.parse(at)).catch(x => null); if (user) { userId = user.id; } else { diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index d6a5b00c0b..b30f87c913 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -8,6 +8,7 @@ import { defineAsyncComponent } from 'vue'; import { i18n } from '@/i18n.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { MenuItem } from '@/types/menu.js'; import { defaultStore } from '@/store.js'; @@ -18,7 +19,7 @@ function rename(file: Misskey.entities.DriveFile) { default: file.name, }).then(({ canceled, result: name }) => { if (canceled) return; - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, name: name, }); @@ -31,7 +32,7 @@ function describe(file: Misskey.entities.DriveFile) { file: file, }, { done: caption => { - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, comment: caption.length === 0 ? null : caption, }); @@ -40,7 +41,7 @@ function describe(file: Misskey.entities.DriveFile) { } function toggleSensitive(file: Misskey.entities.DriveFile) { - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, isSensitive: !file.isSensitive, }).catch(err => { @@ -69,7 +70,7 @@ async function deleteFile(file: Misskey.entities.DriveFile) { }); if (canceled) return; - os.api('drive/files/delete', { + misskeyApi('drive/files/delete', { fileId: file.id, }); } diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index a409f1b775..f6db7c48a6 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -10,6 +10,7 @@ import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { url } from '@/config.js'; import { defaultStore, noteActions } from '@/store.js'; @@ -40,7 +41,7 @@ export async function getNoteClipMenu(props: { action: () => { claimAchievement('noteClipped1'); os.promiseDialog( - os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), + misskeyApi('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), null, async (err) => { if (err.id === '734806c4-542c-463a-9311-15c512803965') { @@ -156,7 +157,7 @@ export function getNoteMenu(props: { }).then(({ canceled }) => { if (canceled) return; - os.api('notes/delete', { + misskeyApi('notes/delete', { noteId: appearNote.id, }); @@ -173,7 +174,7 @@ export function getNoteMenu(props: { }).then(({ canceled }) => { if (canceled) return; - os.api('notes/delete', { + misskeyApi('notes/delete', { noteId: appearNote.id, }); @@ -265,7 +266,7 @@ export function getNoteMenu(props: { async function translate(): Promise<void> { if (props.translation.value != null) return; props.translating.value = true; - const res = await os.api('notes/translate', { + const res = await misskeyApi('notes/translate', { noteId: appearNote.id, targetLang: miLocalStorage.getItem('lang') ?? navigator.language, }); @@ -275,7 +276,7 @@ export function getNoteMenu(props: { let menu: MenuItem[]; if ($i) { - const statePromise = os.api('notes/state', { + const statePromise = misskeyApi('notes/state', { noteId: appearNote.id, }); @@ -355,7 +356,7 @@ export function getNoteMenu(props: { icon: 'ph-user ph-bold ph-lg', text: i18n.ts.user, children: async () => { - const user = appearNote.userId === $i?.id ? $i : await os.api('users/show', { userId: appearNote.userId }); + const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); const { menu, cleanup } = getUserMenu(user); cleanups.push(cleanup); return menu; @@ -377,6 +378,42 @@ export function getNoteMenu(props: { ] : [] ), + ...(appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin) ? [ + { type: 'divider' }, + { + type: 'parent' as const, + icon: 'ti ti-device-tv', + text: i18n.ts.channel, + children: async () => { + const channelChildMenu = [] as MenuItem[]; + + const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id }); + + if (channel.pinnedNoteIds.includes(appearNote.id)) { + channelChildMenu.push({ + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id), + }), + }); + } else { + channelChildMenu.push({ + icon: 'ti ti-pin', + text: i18n.ts.pin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id], + }), + }); + } + return channelChildMenu; + }, + }, + ] + : [] + ), ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ { type: 'divider' }, appearNote.userId === $i.id ? { @@ -497,7 +534,7 @@ export function getRenoteMenu(props: { } if (!props.mock) { - os.api('notes/create', { + misskeyApi('notes/create', { renoteId: appearNote.id, channelId: appearNote.channelId, }).then(() => { @@ -542,7 +579,7 @@ export function getRenoteMenu(props: { } if (!props.mock) { - os.api('notes/create', { + misskeyApi('notes/create', { localOnly, visibility, renoteId: appearNote.id, diff --git a/packages/frontend/src/scripts/get-note-versions-menu.ts b/packages/frontend/src/scripts/get-note-versions-menu.ts index 46e3bab3a7..e05e2e91be 100644 --- a/packages/frontend/src/scripts/get-note-versions-menu.ts +++ b/packages/frontend/src/scripts/get-note-versions-menu.ts @@ -2,6 +2,7 @@ import { Ref, defineAsyncComponent } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from './misskey-api.js'; import { MenuItem } from '@/types/menu.js'; import { dateTimeFormat } from './intl-const.js'; @@ -30,7 +31,7 @@ export async function getNoteVersionsMenu(props: { } const menu: MenuItem[] = []; - const statePromise = os.api('notes/versions', { + const statePromise = misskeyApi('notes/versions', { noteId: appearNote.id, }); diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 67bc781aef..35eeded7e7 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -10,13 +10,14 @@ import { i18n } from '@/i18n.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { host, url } from '@/config.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore, userActions } from '@/store.js'; import { $i, iAmModerator } from '@/account.js'; -import { mainRouter } from '@/router.js'; -import { Router } from '@/nirax.js'; +import { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; +import { mainRouter } from '@/global/router/main.js'; -export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router = mainRouter) { +export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { const meId = $i ? $i.id : null; const cleanups = [] as (() => void)[]; @@ -131,7 +132,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router } async function editMemo(): Promise<void> { - const userDetailed = await os.api('users/show', { + const userDetailed = await misskeyApi('users/show', { userId: user.id, }); const { canceled, result } = await os.form(i18n.ts.editMemo, { diff --git a/packages/frontend/src/scripts/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts index 1310a0dc73..ee218df018 100644 --- a/packages/frontend/src/scripts/install-plugin.ts +++ b/packages/frontend/src/scripts/install-plugin.ts @@ -10,6 +10,7 @@ import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import type { Plugin } from '@/store.js'; import { ColdDeviceStorage } from '@/store.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; export type AiScriptPluginMeta = { @@ -110,7 +111,7 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { }, { done: async result => { const { name, permissions } = result; - const { token } = await os.api('miauth/gen-token', { + const { token } = await misskeyApi('miauth/gen-token', { session: null, name: name, permission: permissions, diff --git a/packages/frontend/src/scripts/lookup-user.ts b/packages/frontend/src/scripts/lookup-user.ts index a35fe898e4..9ae5eccb7c 100644 --- a/packages/frontend/src/scripts/lookup-user.ts +++ b/packages/frontend/src/scripts/lookup-user.ts @@ -6,6 +6,7 @@ import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; export async function lookupUser() { const { canceled, result } = await os.inputText({ @@ -17,8 +18,8 @@ export async function lookupUser() { os.pageWindow(`/admin/user/${user.id}`); }; - const usernamePromise = os.api('users/show', Misskey.acct.parse(result)); - const idPromise = os.api('users/show', { userId: result }); + const usernamePromise = misskeyApi('users/show', Misskey.acct.parse(result)); + const idPromise = misskeyApi('users/show', { userId: result }); let _notFound = false; const notFound = () => { if (_notFound) { diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts index 979f40f038..ddcfd8852e 100644 --- a/packages/frontend/src/scripts/lookup.ts +++ b/packages/frontend/src/scripts/lookup.ts @@ -4,9 +4,10 @@ */ import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { Router } from '@/nirax.js'; +import { mainRouter } from '@/global/router/main.js'; export async function lookup(router?: Router) { const _router = router ?? mainRouter; @@ -28,7 +29,7 @@ export async function lookup(router?: Router) { } if (query.startsWith('https://')) { - const promise = os.api('ap/show', { + const promise = misskeyApi('ap/show', { uri: query, }); diff --git a/packages/frontend/src/scripts/api.ts b/packages/frontend/src/scripts/misskey-api.ts index 8f3a163938..337fa15113 100644 --- a/packages/frontend/src/scripts/api.ts +++ b/packages/frontend/src/scripts/misskey-api.ts @@ -10,12 +10,17 @@ import { $i } from '@/account.js'; export const pendingApiRequestsCount = ref(0); // Implements Misskey.api.ApiClient.request -export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>( +export function misskeyApi< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT, +>( endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal, -): Promise<Misskey.api.SwitchCaseResponseType<E, P>> { +): Promise<_ResT> { if (endpoint.includes('://')) throw new Error('invalid endpoint'); pendingApiRequestsCount.value++; @@ -23,7 +28,7 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin pendingApiRequestsCount.value--; }; - const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => { + const promise = new Promise<_ResT>((resolve, reject) => { // Append a credential if ($i) (data as any).i = $i.token; if (token !== undefined) (data as any).i = token; @@ -44,7 +49,7 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin if (res.status === 200) { resolve(body); } else if (res.status === 204) { - resolve(); + resolve(undefined as _ResT); // void -> undefined } else { reject(body.error); } @@ -57,10 +62,15 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin } // Implements Misskey.api.ApiClient.request -export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>( +export function misskeyApiGet< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT, +>( endpoint: E, data: P = {} as any, -): Promise<Misskey.api.SwitchCaseResponseType<E, P>> { +): Promise<_ResT> { pendingApiRequestsCount.value++; const onFinally = () => { @@ -69,7 +79,7 @@ export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endp const query = new URLSearchParams(data as any); - const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => { + const promise = new Promise<_ResT>((resolve, reject) => { // Send request window.fetch(`${apiUrl}/${endpoint}?${query}`, { method: 'GET', @@ -81,7 +91,7 @@ export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endp if (res.status === 200) { resolve(body); } else if (res.status === 204) { - resolve(); + resolve(undefined as _ResT); // void -> undefined } else { reject(body.error); } diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index 674c762fac..7a2d5a9b9f 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -6,6 +6,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; @@ -65,7 +66,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> { } }); - os.api('drive/files/upload-from-url', { + misskeyApi('drive/files/upload-from-url', { url: url, folderId: defaultStore.state.uploadFolder, marker, diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 2f7545ef0d..690c342c85 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -5,7 +5,6 @@ import type { SoundStore } from '@/store.js'; import { defaultStore } from '@/store.js'; -import * as os from '@/os.js'; let ctx: AudioContext; const cache = new Map<string, AudioBuffer>(); @@ -89,63 +88,35 @@ export type OperationType = typeof operationTypes[number]; /** * 音声を読み込む - * @param soundStore サウンド設定 + * @param url url * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする */ -export async function loadAudio(soundStore: SoundStore, options?: { useCache?: boolean; }) { +export async function loadAudio(url: string, options?: { useCache?: boolean; }) { if (_DEV_) console.log('loading audio. opts:', options); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { - return; - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ctx == null) { ctx = new AudioContext(); } if (options?.useCache ?? true) { - if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) { + if (cache.has(url)) { if (_DEV_) console.log('use cache'); - return cache.get(soundStore.fileId) as AudioBuffer; - } else if (cache.has(soundStore.type)) { - if (_DEV_) console.log('use cache'); - return cache.get(soundStore.type) as AudioBuffer; + return cache.get(url) as AudioBuffer; } } let response: Response; - if (soundStore.type === '_driveFile_') { - try { - response = await fetch(soundStore.fileUrl); - } catch (err) { - try { - // URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック - const apiRes = await os.api('drive/files/show', { - fileId: soundStore.fileId, - }); - response = await fetch(apiRes.url); - } catch (fbErr) { - // それでも無理なら諦める - return; - } - } - } else { - try { - response = await fetch(`/client-assets/sounds/${soundStore.type}.mp3`); - } catch (err) { - return; - } + try { + response = await fetch(url); + } catch (err) { + return; } const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await ctx.decodeAudioData(arrayBuffer); if (options?.useCache ?? true) { - if (soundStore.type === '_driveFile_') { - cache.set(soundStore.fileId, audioBuffer); - } else { - cache.set(soundStore.type, audioBuffer); - } + cache.set(url, audioBuffer); } return audioBuffer; @@ -174,25 +145,46 @@ export function play(operationType: OperationType) { * @param soundStore サウンド設定 */ export async function playFile(soundStore: SoundStore) { - const buffer = await loadAudio(soundStore); + if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { + return; + } + const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`; + const buffer = await loadAudio(url); if (!buffer) return; - createSourceNode(buffer, soundStore.volume)?.start(); + createSourceNode(buffer, soundStore.volume)?.soundSource.start(); } -export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null { +export async function playUrl(url: string, volume = 1, pan = 0, playbackRate = 1) { + const buffer = await loadAudio(url); + if (!buffer) return; + createSourceNode(buffer, volume, pan, playbackRate)?.soundSource.start(); +} + +export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, playbackRate = 1): { + soundSource: AudioBufferSourceNode; + panNode: StereoPannerNode; + gainNode: GainNode; +} | null { const masterVolume = defaultStore.state.sound_masterVolume; if (isMute() || masterVolume === 0 || volume === 0) { return null; } + const panNode = ctx.createStereoPanner(); + panNode.pan.value = pan; + const gainNode = ctx.createGain(); gainNode.gain.value = masterVolume * volume; const soundSource = ctx.createBufferSource(); soundSource.buffer = buffer; - soundSource.connect(gainNode).connect(ctx.destination); + soundSource.playbackRate.value = playbackRate; + soundSource + .connect(panNode) + .connect(gainNode) + .connect(ctx.destination); - return soundSource; + return { soundSource, panNode, gainNode }; } /** diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index b896376ec8..14b667fd68 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -5,7 +5,7 @@ import { reactive, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { readAndCompressImage } from 'browser-image-resizer'; +import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; import { getCompressionConfig } from './upload/compress-config.js'; import { defaultStore } from '@/store.js'; import { apiUrl } from '@/config.js'; diff --git a/packages/frontend/src/scripts/upload/compress-config.ts b/packages/frontend/src/scripts/upload/compress-config.ts index 2deb9cbb81..4775213c20 100644 --- a/packages/frontend/src/scripts/upload/compress-config.ts +++ b/packages/frontend/src/scripts/upload/compress-config.ts @@ -5,7 +5,7 @@ import isAnimated from 'is-file-animated'; import { isWebpSupported } from './isWebpSupported.js'; -import type { BrowserImageResizerConfig } from 'browser-image-resizer'; +import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer'; const compressTypeMap = { 'image/jpeg': { quality: 0.90, mimeType: 'image/webp' }, @@ -21,7 +21,7 @@ const compressTypeMapFallback = { 'image/svg+xml': { quality: 1, mimeType: 'image/png' }, } as const; -export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfig | undefined> { +export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfigWithConvertedOutput | undefined> { const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type]; if (!imgConfig || await isAnimated(file)) { return; diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts index bcdba5455a..fb31fce1de 100644 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -8,6 +8,7 @@ import * as Misskey from 'misskey-js'; import { useStream } from '@/stream.js'; import { $i } from '@/account.js'; import * as os from '@/os.js'; +import { misskeyApi } from './misskey-api.js'; export function useNoteCapture(props: { rootEl: Ref<HTMLElement>; @@ -32,7 +33,7 @@ export function useNoteCapture(props: { // notes/show may throw if the current user can't see the note try { - const replyNote = await os.api('notes/show', { + const replyNote = await misskeyApi('notes/show', { noteId: body.id, }); @@ -100,7 +101,7 @@ export function useNoteCapture(props: { case 'updated': { try { - const editedNote = await os.api('notes/show', { + const editedNote = await misskeyApi('notes/show', { noteId: id, }); diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts index f37c01cca1..f96059b849 100644 --- a/packages/frontend/src/theme-store.ts +++ b/packages/frontend/src/theme-store.ts @@ -5,7 +5,7 @@ import { Theme, getBuiltinThemes } from '@/scripts/theme.js'; import { miLocalStorage } from '@/local-storage.js'; -import { api } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; const lsCacheKey = $i ? `themes:${$i.id}` as const : null; @@ -19,7 +19,7 @@ export async function fetchThemes(): Promise<void> { if ($i == null) return; try { - const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' }); + const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }); miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes)); } catch (err) { if (err.code === 'NO_SUCH_KEY') return; @@ -35,13 +35,13 @@ export async function addTheme(theme: Theme): Promise<void> { } await fetchThemes(); const themes = getThemes().concat(theme); - await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); + await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes)); } export async function removeTheme(theme: Theme): Promise<void> { if ($i == null) return; const themes = getThemes().filter(t => t.id !== theme.id); - await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); + await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes)); } diff --git a/packages/frontend/src/type.ts b/packages/frontend/src/type.ts new file mode 100644 index 0000000000..9c0fc2a11e --- /dev/null +++ b/packages/frontend/src/type.ts @@ -0,0 +1,3 @@ +export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }; + +export type WithNonNullable<T, K extends keyof T> = T & { [P in K]-?: NonNullable<T[P]> }; diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index a3adbfb1b1..3859b0cf62 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -27,6 +27,11 @@ function toolsMenuItems(): MenuItem[] { to: '/clicker', text: '🍪👈', icon: 'ph-cookie ph-bold ph-lg', + }, { + type: 'link', + to: '/bubble-game', + text: i18n.ts.bubbleGame, + icon: 'ph-apple ph-bold ph-lg', }, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? { type: 'link', to: '/custom-emojis-manager', diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 6ece7d86d7..78af49cdc2 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -49,7 +49,8 @@ import { defineAsyncComponent, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { swInject } from './sw-inject.js'; import XNotification from './notification.vue'; -import { popups, pendingApiRequestsCount } from '@/os.js'; +import { popups } from '@/os.js'; +import { pendingApiRequestsCount } from '@/scripts/misskey-api.js'; import { uploads } from '@/scripts/upload.js'; import * as sound from '@/scripts/sound.js'; import { $i } from '@/account.js'; diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue index c92695afed..8df3b289de 100644 --- a/packages/frontend/src/ui/_common_/statusbar-federation.vue +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useInterval } from '@/scripts/use-interval.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; @@ -52,7 +52,7 @@ const fetching = ref(true); const key = ref(0); const tick = () => { - os.api('federation/instances', { + misskeyApi('federation/instances', { sort: '+latestRequestReceivedAt', limit: 30, }).then(res => { diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue index 6057174ba8..34d7b0e4e5 100644 --- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue +++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useInterval } from '@/scripts/use-interval.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; import { notePage } from '@/filters/note.js'; @@ -54,7 +54,7 @@ const key = ref(0); const tick = () => { if (props.userListId == null) return; - os.api('notes/user-list-timeline', { + misskeyApi('notes/user-list-timeline', { listId: props.userListId, }).then(res => { notes.value = res; diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts index 5239b76705..4c77465eb1 100644 --- a/packages/frontend/src/ui/_common_/sw-inject.ts +++ b/packages/frontend/src/ui/_common_/sw-inject.ts @@ -3,11 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { api, post } from '@/os.js'; +import { post } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i, login } from '@/account.js'; import { getAccountFromId } from '@/scripts/get-account-from-id.js'; -import { mainRouter } from '@/router.js'; import { deepClone } from '@/scripts/clone.js'; +import { mainRouter } from '@/global/router/main.js'; export function swInject() { navigator.serviceWorker.addEventListener('message', async ev => { @@ -30,10 +31,10 @@ export function swInject() { // プッシュ通知から来たreply,renoteはtruncateBodyが通されているため、 // 完全なノートを取得しなおす if (props.reply) { - props.reply = await api('notes/show', { noteId: props.reply.id }); + props.reply = await misskeyApi('notes/show', { noteId: props.reply.id }); } if (props.renote) { - props.renote = await api('notes/show', { noteId: props.renote.id }); + props.renote = await misskeyApi('notes/show', { noteId: props.renote.id }); } return post(props); } diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index 3bb9097985..959a135cc6 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -52,11 +52,11 @@ import XCommon from './_common_/common.vue'; import { instanceName } from '@/config.js'; import { StickySidebar } from '@/scripts/sticky-sidebar.js'; import * as os from '@/os.js'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; +import { mainRouter } from '@/global/router/main.js'; const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue')); const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 0df814fc88..c592d01fbf 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -103,7 +103,6 @@ import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { defaultStore } from '@/store.js'; @@ -117,6 +116,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue'; import XMentionsColumn from '@/ui/deck/mentions-column.vue'; import XDirectColumn from '@/ui/deck/direct-column.vue'; import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; +import { mainRouter } from '@/global/router/main.js'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index 7cd1d6aee9..5c927691d6 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -19,6 +19,7 @@ import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -35,7 +36,7 @@ onMounted(() => { }); async function setAntenna() { - const antennas = await os.api('antennas/list'); + const antennas = await misskeyApi('antennas/list'); const { canceled, result: antenna } = await os.select({ title: i18n.ts.selectAntenna, items: antennas.map(x => ({ diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index 95ed900f7d..7293f82b28 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -26,6 +26,7 @@ import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -41,7 +42,7 @@ if (props.column.channelId == null) { } async function setChannel() { - const channels = await os.api('channels/my-favorites', { + const channels = await misskeyApi('channels/my-favorites', { limit: 100, }); const { canceled, result: channel } = await os.select({ @@ -60,7 +61,7 @@ async function setChannel() { async function post() { if (!channel.value || channel.value.id !== props.column.channelId) { - channel.value = await os.api('channels/show', { + channel.value = await misskeyApi('channels/show', { channelId: props.column.channelId, }); } diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index e68b7bba8c..ae68029cd8 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -7,7 +7,7 @@ import { throttle } from 'throttle-debounce'; import { markRaw } from 'vue'; import { notificationTypes } from 'misskey-js'; import { Storage } from '@/pizzax.js'; -import { api } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { deepClone } from '@/scripts/clone.js'; type ColumnWidget = { @@ -70,7 +70,7 @@ export const loadDeck = async () => { let deck; try { - deck = await api('i/registry/get', { + deck = await misskeyApi('i/registry/get', { scope: ['client', 'deck', 'profiles'], key: deckStore.state.profile, }); @@ -95,7 +95,7 @@ export const loadDeck = async () => { // TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する export const saveDeck = throttle(1000, () => { - api('i/registry/set', { + misskeyApi('i/registry/set', { scope: ['client', 'deck', 'profiles'], key: deckStore.state.profile, value: { @@ -106,13 +106,13 @@ export const saveDeck = throttle(1000, () => { }); export async function getProfiles(): Promise<string[]> { - return await api('i/registry/keys', { + return await misskeyApi('i/registry/keys', { scope: ['client', 'deck', 'profiles'], }); } export async function deleteProfile(key: string): Promise<void> { - return await api('i/registry/remove', { + return await misskeyApi('i/registry/remove', { scope: ['client', 'deck', 'profiles'], key: key, }); diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index 45ecc476e7..a869e2743b 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -19,6 +19,7 @@ import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -40,7 +41,7 @@ watch(withRenotes, v => { }); async function setList() { - const lists = await os.api('users/lists/list'); + const lists = await misskeyApi('users/lists/list'); const { canceled, result: list } = await os.select({ title: i18n.ts.selectList, items: lists.map(x => ({ diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index cd567040f4..0ea6a7f23b 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -24,10 +24,10 @@ import XColumn from './column.vue'; import { deckStore, Column } from '@/ui/deck/deck-store.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { useScrollPositionManager } from '@/nirax.js'; import { getScrollContainer } from '@/scripts/scroll.js'; +import { mainRouter } from '@/global/router/main.js'; defineProps<{ column: Column; diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index 5fbd1389b7..e5bffcc4e0 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -19,6 +19,7 @@ import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -35,7 +36,7 @@ onMounted(() => { }); async function setRole() { - const roles = (await os.api('roles/list')).filter(x => x.isExplorable); + const roles = (await misskeyApi('roles/list')).filter(x => x.isExplorable); const { canceled, result: role } = await os.select({ title: i18n.ts.role, items: roles.map(x => ({ diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue index f32f2de3df..b0a2aa35f9 100644 --- a/packages/frontend/src/ui/minimum.vue +++ b/packages/frontend/src/ui/minimum.vue @@ -16,9 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { provide, ComputedRef, ref } from 'vue'; import XCommon from './_common_/common.vue'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { instanceName } from '@/config.js'; +import { mainRouter } from '@/global/router/main.js'; const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 1d8e26bfcc..d0c6357b2a 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -105,12 +105,12 @@ import { defaultStore } from '@/store.js'; import { navbarItemDef } from '@/navbar.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; import { CURRENT_STICKY_BOTTOM } from '@/const.js'; import { useScrollPositionManager } from '@/nirax.js'; +import { mainRouter } from '@/global/router/main.js'; const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue')); diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index 78d7e23689..da725292fd 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -72,14 +72,15 @@ import * as Misskey from 'misskey-js'; import XCommon from './_common_/common.vue'; import { instanceName } from '@/config.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { instance } from '@/instance.js'; import XSigninDialog from '@/components/MkSigninDialog.vue'; import XSignupDialog from '@/components/MkSignupDialog.vue'; import { ColdDeviceStorage, defaultStore } from '@/store.js'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue'; +import { mainRouter } from '@/global/router/main.js'; const DESKTOP_THRESHOLD = 1100; @@ -119,7 +120,7 @@ const keymap = computed(() => { const root = computed(() => mainRouter.currentRoute.value.name === 'index'); -os.api('meta', { detail: true }).then(res => { +misskeyApi('meta', { detail: true }).then(res => { meta.value = res; }); diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue index 9f92f78764..59549c781d 100644 --- a/packages/frontend/src/ui/zen.vue +++ b/packages/frontend/src/ui/zen.vue @@ -24,10 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { provide, ComputedRef, ref } from 'vue'; import XCommon from './_common_/common.vue'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { instanceName, ui } from '@/config.js'; import { i18n } from '@/i18n.js'; +import { mainRouter } from '@/global/router/main.js'; const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue index d2842143b1..d4e68146e2 100644 --- a/packages/frontend/src/widgets/WidgetActivity.vue +++ b/packages/frontend/src/widgets/WidgetActivity.vue @@ -25,7 +25,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid import XCalendar from './WidgetActivity.calendar.vue'; import XChart from './WidgetActivity.chart.vue'; import { GetFormResultType } from '@/scripts/form.js'; -import * as os from '@/os.js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; @@ -76,7 +76,7 @@ const toggleView = () => { save(); }; -os.apiGet('charts/user/notes', { +misskeyApiGet('charts/user/notes', { userId: $i.id, span: 'day', limit: 7 * 21, diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue index c17e9728a5..351a569996 100644 --- a/packages/frontend/src/widgets/WidgetAiscript.vue +++ b/packages/frontend/src/widgets/WidgetAiscript.vue @@ -25,7 +25,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid import { GetFormResultType } from '@/scripts/form.js'; import * as os from '@/os.js'; import MkContainer from '@/components/MkContainer.vue'; -import { createAiScriptEnv } from '@/scripts/aiscript/api.js'; +import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; @@ -69,19 +69,7 @@ const run = async () => { storageKey: 'widget', token: $i?.token, }), { - in: (q) => { - return new Promise(ok => { - os.inputText({ - title: q, - }).then(({ canceled, result: a }) => { - if (canceled) { - ok(''); - } else { - ok(a); - } - }); - }); - }, + in: aiScriptReadline, out: (value) => { logs.value.push({ id: Math.random().toString(), diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue index 10248a840a..e236253797 100644 --- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue +++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue @@ -18,7 +18,7 @@ import { Interpreter, Parser } from '@syuilo/aiscript'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import * as os from '@/os.js'; -import { createAiScriptEnv } from '@/scripts/aiscript/api.js'; +import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; import { $i } from '@/account.js'; import MkAsUi from '@/components/MkAsUi.vue'; import MkContainer from '@/components/MkContainer.vue'; @@ -64,19 +64,7 @@ async function run() { root.value = _root.value; }), }, { - in: (q) => { - return new Promise(ok => { - os.inputText({ - title: q, - }).then(({ canceled, result: a }) => { - if (canceled) { - ok(''); - } else { - ok(a); - } - }); - }); - }, + in: aiScriptReadline, out: (value) => { // nop }, diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index 0a83eba9c1..cd3bc03eb9 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useInterval } from '@/scripts/use-interval.js'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; @@ -70,7 +70,7 @@ const fetch = () => { now.setHours(0, 0, 0, 0); if (now > lfAtD) { - os.api('users/following', { + misskeyApi('users/following', { limit: 18, birthday: now.toISOString(), userId: $i.id, diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue index 11082c1e3f..80fd000d09 100644 --- a/packages/frontend/src/widgets/WidgetButton.vue +++ b/packages/frontend/src/widgets/WidgetButton.vue @@ -16,7 +16,7 @@ import { Interpreter, Parser } from '@syuilo/aiscript'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import * as os from '@/os.js'; -import { createAiScriptEnv } from '@/scripts/aiscript/api.js'; +import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; import { $i } from '@/account.js'; import MkButton from '@/components/MkButton.vue'; @@ -56,19 +56,7 @@ const run = async () => { storageKey: 'widget', token: $i?.token, }), { - in: (q) => { - return new Promise(ok => { - os.inputText({ - title: q, - }).then(({ canceled, result: a }) => { - if (canceled) { - ok(''); - } else { - ok(a); - } - }); - }); - }, + in: aiScriptReadline, out: (value) => { // nop }, diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index 9be7d084e9..dd849c1ae5 100644 --- a/packages/frontend/src/widgets/WidgetFederation.vue +++ b/packages/frontend/src/widgets/WidgetFederation.vue @@ -31,7 +31,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; -import * as os from '@/os.js'; +import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import { useInterval } from '@/scripts/use-interval.js'; import { i18n } from '@/i18n.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; @@ -62,11 +62,11 @@ const charts = ref<Misskey.entities.ChartsInstanceResponse[]>([]); const fetching = ref(true); const fetch = async () => { - const fetchedInstances = await os.api('federation/instances', { + const fetchedInstances = await misskeyApi('federation/instances', { sort: '+latestRequestReceivedAt', limit: 5, }); - const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.apiGet('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); + const fetchedCharts = await Promise.all(fetchedInstances.map(i => misskeyApiGet('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); instances.value = fetchedInstances; charts.value = fetchedCharts; fetching.value = false; diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue index 38323ed040..800cf71de0 100644 --- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue +++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue @@ -25,6 +25,7 @@ import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkTagCloud from '@/components/MkTagCloud.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useInterval } from '@/scripts/use-interval.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; @@ -56,7 +57,7 @@ function onInstanceClick(i) { } useInterval(() => { - os.api('federation/instances', { + misskeyApi('federation/instances', { sort: '+latestRequestReceivedAt', limit: 25, }).then(res => { diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index c54682bb87..e5d8a3e5ea 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -10,19 +10,19 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="values"> <div> <div>Process</div> - <div :class="{ inc: current.inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: current.inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(current.inbox.activeSincePrevTick) }}</div> + <div :class="{ inc: current.inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: current.inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }" :title="`${current.inbox.activeSincePrevTick}`">{{ kmg(current.inbox.activeSincePrevTick, 2) }}</div> </div> <div> <div>Active</div> - <div :class="{ inc: current.inbox.active > prev.inbox.active, dec: current.inbox.active < prev.inbox.active }">{{ number(current.inbox.active) }}</div> + <div :class="{ inc: current.inbox.active > prev.inbox.active, dec: current.inbox.active < prev.inbox.active }" :title="`${current.inbox.active}`">{{ kmg(current.inbox.active, 2) }}</div> </div> <div> <div>Delayed</div> - <div :class="{ inc: current.inbox.delayed > prev.inbox.delayed, dec: current.inbox.delayed < prev.inbox.delayed }">{{ number(current.inbox.delayed) }}</div> + <div :class="{ inc: current.inbox.delayed > prev.inbox.delayed, dec: current.inbox.delayed < prev.inbox.delayed }" :title="`${current.inbox.delayed}`">{{ kmg(current.inbox.delayed, 2) }}</div> </div> <div> <div>Waiting</div> - <div :class="{ inc: current.inbox.waiting > prev.inbox.waiting, dec: current.inbox.waiting < prev.inbox.waiting }">{{ number(current.inbox.waiting) }}</div> + <div :class="{ inc: current.inbox.waiting > prev.inbox.waiting, dec: current.inbox.waiting < prev.inbox.waiting }" :title="`${current.inbox.waiting}`">{{ kmg(current.inbox.waiting, 2) }}</div> </div> </div> </div> @@ -31,19 +31,19 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="values"> <div> <div>Process</div> - <div :class="{ inc: current.deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: current.deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(current.deliver.activeSincePrevTick) }}</div> + <div :class="{ inc: current.deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: current.deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }" :title="`${current.deliver.activeSincePrevTick}`">{{ kmg(current.deliver.activeSincePrevTick, 2) }}</div> </div> <div> <div>Active</div> - <div :class="{ inc: current.deliver.active > prev.deliver.active, dec: current.deliver.active < prev.deliver.active }">{{ number(current.deliver.active) }}</div> + <div :class="{ inc: current.deliver.active > prev.deliver.active, dec: current.deliver.active < prev.deliver.active }" :title="`${current.deliver.active}`">{{ kmg(current.deliver.active, 2) }}</div> </div> <div> <div>Delayed</div> - <div :class="{ inc: current.deliver.delayed > prev.deliver.delayed, dec: current.deliver.delayed < prev.deliver.delayed }">{{ number(current.deliver.delayed) }}</div> + <div :class="{ inc: current.deliver.delayed > prev.deliver.delayed, dec: current.deliver.delayed < prev.deliver.delayed }" :title="`${current.deliver.delayed}`">{{ kmg(current.deliver.delayed, 2) }}</div> </div> <div> <div>Waiting</div> - <div :class="{ inc: current.deliver.waiting > prev.deliver.waiting, dec: current.deliver.waiting < prev.deliver.waiting }">{{ number(current.deliver.waiting) }}</div> + <div :class="{ inc: current.deliver.waiting > prev.deliver.waiting, dec: current.deliver.waiting < prev.deliver.waiting }" :title="`${current.deliver.waiting}`">{{ kmg(current.deliver.waiting, 2) }}</div> </div> </div> </div> @@ -55,7 +55,7 @@ import { onUnmounted, reactive, ref } from 'vue'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import { useStream } from '@/stream.js'; -import number from '@/filters/number.js'; +import kmg from '@/filters/kmg.js'; import * as sound from '@/scripts/sound.js'; import { deepClone } from '@/scripts/clone.js'; import { defaultStore } from '@/store.js'; @@ -104,10 +104,7 @@ const jammedAudioBuffer = ref<AudioBuffer | null>(null); const jammedSoundNodePlaying = ref<boolean>(false); if (defaultStore.state.sound_masterVolume) { - sound.loadAudio({ - type: 'syuilo/queue-jammed', - volume: 1, - }).then(buf => { + sound.loadAudio('/client-assets/sounds/syuilo/queue-jammed.mp3').then(buf => { if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer'); jammedAudioBuffer.value = buf; }); @@ -126,7 +123,7 @@ const onStats = (stats) => { current[domain].delayed = stats[domain].delayed; if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer.value && !jammedSoundNodePlaying.value) { - const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1); + const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1)?.soundSource; if (soundNode) { jammedSoundNodePlaying.value = true; soundNode.onended = () => jammedSoundNodePlaying.value = false; diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue index 0a6fec7f2e..e544a39d55 100644 --- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue +++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; -import * as os from '@/os.js'; +import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import { useInterval } from '@/scripts/use-interval.js'; import { i18n } from '@/i18n.js'; import number from '@/filters/number.js'; @@ -45,7 +45,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name, const onlineUsersCount = ref(0); const tick = () => { - os.apiGet('get-online-users-count').then(res => { + misskeyApiGet('get-online-users-count').then(res => { onlineUsersCount.value = res.count; }); }; diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue index ff9b6e19f5..db879f3dba 100644 --- a/packages/frontend/src/widgets/WidgetPhotos.vue +++ b/packages/frontend/src/widgets/WidgetPhotos.vue @@ -28,7 +28,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid import { GetFormResultType } from '@/scripts/form.js'; import { useStream } from '@/stream.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; @@ -74,7 +74,7 @@ const thumbnail = (image: any): string => { : image.thumbnailUrl; }; -os.api('drive/stream', { +misskeyApi('drive/stream', { type: 'image/*', limit: 9, }).then(res => { diff --git a/packages/frontend/src/widgets/WidgetSearch.vue b/packages/frontend/src/widgets/WidgetSearch.vue index 9999139776..91f4b58912 100644 --- a/packages/frontend/src/widgets/WidgetSearch.vue +++ b/packages/frontend/src/widgets/WidgetSearch.vue @@ -20,8 +20,9 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid import MkInput from '@/components/MkInput.vue'; import MkContainer from '@/components/MkContainer.vue'; import { i18n } from '@/i18n.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import * as os from '@/os.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; import { GetFormResultType } from '@/scripts/form.js'; const name = 'search'; @@ -100,7 +101,7 @@ async function search() { if (query == null || query === '') return; if (query.startsWith('https://')) { - const promise = os.api('ap/show', { + const promise = misskeyApi('ap/show', { uri: query, }); diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue index 7e39a05881..94bf6d7eec 100644 --- a/packages/frontend/src/widgets/WidgetSlideshow.vue +++ b/packages/frontend/src/widgets/WidgetSlideshow.vue @@ -22,6 +22,7 @@ import * as Misskey from 'misskey-js'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useInterval } from '@/scripts/use-interval.js'; import { i18n } from '@/i18n.js'; @@ -77,7 +78,7 @@ const change = () => { const fetch = () => { fetching.value = true; - os.api('drive/files', { + misskeyApi('drive/files', { folderId: widgetProps.folderId, type: 'image/*', limit: 100, diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue index 070466f476..a11309a77d 100644 --- a/packages/frontend/src/widgets/WidgetTimeline.vue +++ b/packages/frontend/src/widgets/WidgetTimeline.vue @@ -39,6 +39,7 @@ import { ref } from 'vue'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import { i18n } from '@/i18n.js'; @@ -97,8 +98,8 @@ const setSrc = (src) => { const choose = async (ev) => { menuOpened.value = true; const [antennas, lists] = await Promise.all([ - os.api('antennas/list'), - os.api('users/lists/list'), + misskeyApi('antennas/list'), + misskeyApi('users/lists/list'), ]); const antennaItems = antennas.map(antenna => ({ text: antenna.name, diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue index 3416a1c0a7..6fc04d2719 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -30,7 +30,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; -import * as os from '@/os.js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { useInterval } from '@/scripts/use-interval.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; @@ -59,7 +59,7 @@ const stats = ref<Misskey.entities.HashtagsTrendResponse>([]); const fetching = ref(true); const fetch = () => { - os.apiGet('hashtags/trend').then(res => { + misskeyApiGet('hashtags/trend').then(res => { stats.value = res; fetching.value = false; }); diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue index c40328d2fa..d021502877 100644 --- a/packages/frontend/src/widgets/WidgetUserList.vue +++ b/packages/frontend/src/widgets/WidgetUserList.vue @@ -30,6 +30,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useInterval } from '@/scripts/use-interval.js'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; @@ -64,7 +65,7 @@ const users = ref<Misskey.entities.UserDetailed[]>([]); const fetching = ref(true); async function chooseList() { - const lists = await os.api('users/lists/list'); + const lists = await misskeyApi('users/lists/list'); const { canceled, result: list } = await os.select({ title: i18n.ts.selectList, items: lists.map(x => ({ @@ -85,11 +86,11 @@ const fetch = () => { return; } - os.api('users/lists/show', { + misskeyApi('users/lists/show', { listId: widgetProps.listId, }).then(_list => { list.value = _list; - os.api('users/show', { + misskeyApi('users/show', { userIds: list.value.userIds, }).then(_users => { users.value = _users; diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue index f13b6a370d..ee720bd9d7 100644 --- a/packages/frontend/src/widgets/server-metric/cpu-mem.vue +++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue @@ -80,13 +80,13 @@ import * as Misskey from 'misskey-js'; import { v4 as uuid } from 'uuid'; const props = defineProps<{ - connection: any, + connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, meta: Misskey.entities.ServerInfoResponse }>(); const viewBoxX = ref<number>(50); const viewBoxY = ref<number>(30); -const stats = ref<any[]>([]); +const stats = ref<Misskey.entities.ServerStats[]>([]); const cpuGradientId = uuid(); const cpuMaskId = uuid(); const memGradientId = uuid(); @@ -107,6 +107,7 @@ onMounted(() => { props.connection.on('statsLog', onStatsLog); props.connection.send('requestLog', { id: Math.random().toString().substring(2, 10), + length: 50, }); }); @@ -115,7 +116,7 @@ onBeforeUnmount(() => { props.connection.off('statsLog', onStatsLog); }); -function onStats(connStats) { +function onStats(connStats: Misskey.entities.ServerStats) { stats.value.push(connStats); if (stats.value.length > 50) stats.value.shift(); @@ -136,8 +137,8 @@ function onStats(connStats) { memP.value = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0); } -function onStatsLog(statsLog) { - for (const revStats of [...statsLog].reverse()) { +function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) { + for (const revStats of statsLog.reverse()) { onStats(revStats); } } diff --git a/packages/frontend/src/widgets/server-metric/cpu.vue b/packages/frontend/src/widgets/server-metric/cpu.vue index 35c20c8935..36b2cd3b04 100644 --- a/packages/frontend/src/widgets/server-metric/cpu.vue +++ b/packages/frontend/src/widgets/server-metric/cpu.vue @@ -20,13 +20,13 @@ import * as Misskey from 'misskey-js'; import XPie from './pie.vue'; const props = defineProps<{ - connection: any, + connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, meta: Misskey.entities.ServerInfoResponse }>(); const usage = ref<number>(0); -function onStats(stats) { +function onStats(stats: Misskey.entities.ServerStats) { usage.value = stats.cpu; } diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue index 9a785d9112..176343772e 100644 --- a/packages/frontend/src/widgets/server-metric/index.vue +++ b/packages/frontend/src/widgets/server-metric/index.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onUnmounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from '../widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget.js'; import XCpuMemory from './cpu-mem.vue'; import XNet from './net.vue'; import XCpu from './cpu.vue'; @@ -30,7 +30,7 @@ import XMemory from './mem.vue'; import XDisk from './disk.vue'; import MkContainer from '@/components/MkContainer.vue'; import { GetFormResultType } from '@/scripts/form.js'; -import * as os from '@/os.js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; @@ -54,11 +54,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure, save } = useWidgetPropsManager(name, widgetPropsDef, @@ -68,7 +65,7 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name, const meta = ref<Misskey.entities.ServerInfoResponse | null>(null); -os.apiGet('server-info', {}).then(res => { +misskeyApiGet('server-info', {}).then(res => { meta.value = res; }); diff --git a/packages/frontend/src/widgets/server-metric/mem.vue b/packages/frontend/src/widgets/server-metric/mem.vue index 34a1f1ae3d..7a43cee59b 100644 --- a/packages/frontend/src/widgets/server-metric/mem.vue +++ b/packages/frontend/src/widgets/server-metric/mem.vue @@ -22,7 +22,7 @@ import XPie from './pie.vue'; import bytes from '@/filters/bytes.js'; const props = defineProps<{ - connection: any, + connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, meta: Misskey.entities.ServerInfoResponse }>(); @@ -31,7 +31,7 @@ const total = ref<number>(0); const used = ref<number>(0); const free = ref<number>(0); -function onStats(stats) { +function onStats(stats: Misskey.entities.ServerStats) { usage.value = stats.mem.active / props.meta.mem.total; total.value = props.meta.mem.total; used.value = stats.mem.active; diff --git a/packages/frontend/src/widgets/server-metric/net.vue b/packages/frontend/src/widgets/server-metric/net.vue index 7af88a94eb..d33c2c577d 100644 --- a/packages/frontend/src/widgets/server-metric/net.vue +++ b/packages/frontend/src/widgets/server-metric/net.vue @@ -54,13 +54,13 @@ import * as Misskey from 'misskey-js'; import bytes from '@/filters/bytes.js'; const props = defineProps<{ - connection: any, + connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, meta: Misskey.entities.ServerInfoResponse }>(); const viewBoxX = ref<number>(50); const viewBoxY = ref<number>(30); -const stats = ref<any[]>([]); +const stats = ref<Misskey.entities.ServerStats[]>([]); const inPolylinePoints = ref<string>(''); const outPolylinePoints = ref<string>(''); const inPolygonPoints = ref<string>(''); @@ -77,6 +77,7 @@ onMounted(() => { props.connection.on('statsLog', onStatsLog); props.connection.send('requestLog', { id: Math.random().toString().substring(2, 10), + length: 50, }); }); @@ -85,7 +86,7 @@ onBeforeUnmount(() => { props.connection.off('statsLog', onStatsLog); }); -function onStats(connStats) { +function onStats(connStats: Misskey.entities.ServerStats) { stats.value.push(connStats); if (stats.value.length > 50) stats.value.shift(); @@ -109,8 +110,8 @@ function onStats(connStats) { outRecent.value = connStats.net.tx; } -function onStatsLog(statsLog) { - for (const revStats of [...statsLog].reverse()) { +function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) { + for (const revStats of statsLog.reverse()) { onStats(revStats); } } diff --git a/packages/frontend/test/emoji.test.ts b/packages/frontend/test/emoji.test.ts new file mode 100644 index 0000000000..a1782a4913 --- /dev/null +++ b/packages/frontend/test/emoji.test.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, test, assert, afterEach } from 'vitest'; +import { render, cleanup, type RenderResult } from '@testing-library/vue'; +import { defaultStoreState } from './init.js'; +import { getEmojiName } from '@/scripts/emojilist.js'; +import { components } from '@/components/index.js'; +import { directives } from '@/directives/index.js'; +import MkEmoji from '@/components/global/MkEmoji.vue'; + +describe('Emoji', () => { + const renderEmoji = (emoji: string): RenderResult => { + return render(MkEmoji, { + props: { emoji }, + global: { directives, components }, + }); + }; + + afterEach(() => { + cleanup(); + defaultStoreState.emojiStyle = ''; + }); + + describe('MkEmoji', () => { + test('Should render selector-less heart with color in native mode', async () => { + defaultStoreState.emojiStyle = 'native'; + const mkEmoji = await renderEmoji('\u2764'); // monochrome heart + assert.ok(mkEmoji.queryByText('\u2764\uFE0F')); // colored heart + assert.ok(!mkEmoji.queryByText('\u2764')); + }); + }); + + describe('Emoji list', () => { + test('Should get the name of the heart', () => { + assert.strictEqual(getEmojiName('\u2764'), 'heart'); + }); + }); +}); diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts index 6d93ff8cb0..f21248cfee 100644 --- a/packages/frontend/test/init.ts +++ b/packages/frontend/test/init.ts @@ -17,21 +17,23 @@ updateI18n(locales['en-US']); // XXX: misskey-js panics if WebSocket is not defined vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; }); +export const defaultStoreState: Record<string, unknown> = { + + // なんかtestがうまいこと動かないのでここに書く + dataSaver: { + media: false, + avatar: false, + urlPreview: false, + code: false, + }, + +}; + // XXX: defaultStore somehow becomes undefined in vitest? vi.mock('@/store.js', () => { return { defaultStore: { - state: { - - // なんかtestがうまいこと動かないのでここに書く - dataSaver: { - media: false, - avatar: false, - urlPreview: false, - code: false, - }, - - }, + state: defaultStoreState, }, }; }); diff --git a/packages/frontend/test/url-preview.test.ts b/packages/frontend/test/url-preview.test.ts index f760de9274..6cf8317c07 100644 --- a/packages/frontend/test/url-preview.test.ts +++ b/packages/frontend/test/url-preview.test.ts @@ -6,7 +6,7 @@ import { describe, test, assert, afterEach } from 'vitest'; import { render, cleanup, type RenderResult } from '@testing-library/vue'; import './init'; -import type { summaly } from 'summaly'; +import type { summaly } from '@misskey-dev/summaly'; import { components } from '@/components/index.js'; import { directives } from '@/directives/index.js'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index d4c43f207c..a79ea1b420 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1034,6 +1034,18 @@ export type Endpoints = Overwrite<Endpoints_2, { }; }; }; + 'signup': { + req: SignupRequest; + res: SignupResponse; + }; + 'signup-pending': { + req: SignupPendingRequest; + res: SignupPendingResponse; + }; + 'signin': { + req: SigninRequest; + res: SigninResponse; + }; }>; // @public (undocumented) @@ -1053,6 +1065,12 @@ declare namespace entities { EmojiUpdated, EmojiDeleted, AnnouncementCreated, + SignupRequest, + SignupResponse, + SignupPendingRequest, + SignupPendingResponse, + SigninRequest, + SigninResponse, EmptyRequest, EmptyResponse, AdminMetaResponse, @@ -2536,7 +2554,7 @@ type QueueStats = { }; // @public (undocumented) -type QueueStatsLog = string[]; +type QueueStatsLog = QueueStats[]; // @public (undocumented) type RenoteMuteCreateRequest = operations['renote-mute/create']['requestBody']['content']['application/json']; @@ -2610,12 +2628,53 @@ type ServerStats = { }; // @public (undocumented) -type ServerStatsLog = string[]; +type ServerStatsLog = ServerStats[]; // @public (undocumented) type Signin = components['schemas']['Signin']; // @public (undocumented) +type SigninRequest = { + username: string; + password: string; + token?: string; +}; + +// @public (undocumented) +type SigninResponse = { + id: User['id']; + i: string; +}; + +// @public (undocumented) +type SignupPendingRequest = { + code: string; +}; + +// @public (undocumented) +type SignupPendingResponse = { + id: User['id']; + i: string; +}; + +// @public (undocumented) +type SignupRequest = { + username: string; + password: string; + host?: string; + invitationCode?: string; + emailAddress?: string; + 'hcaptcha-response'?: string | null; + 'g-recaptcha-response'?: string | null; + 'turnstile-response'?: string | null; +}; + +// @public (undocumented) +type SignupResponse = MeDetailed & { + token: string; +}; + +// @public (undocumented) type StatsResponse = operations['stats']['responses']['200']['content']['application/json']; // Warning: (ae-forgotten-export) The symbol "StreamEvents" needs to be exported by the entry point index.d.ts diff --git a/packages/misskey-js/generator/package.json b/packages/misskey-js/generator/package.json index 50b23f5792..9c15965b12 100644 --- a/packages/misskey-js/generator/package.json +++ b/packages/misskey-js/generator/package.json @@ -8,15 +8,16 @@ }, "devDependencies": { "@apidevtools/swagger-parser": "10.1.0", + "@misskey-dev/eslint-plugin": "^1.0.0", "@types/node": "20.9.1", "@typescript-eslint/eslint-plugin": "6.11.0", "@typescript-eslint/parser": "6.11.0", "eslint": "8.53.0", - "typescript": "5.3.3", - "tsx": "4.4.0", - "ts-case-convert": "2.0.2", "openapi-types": "12.1.3", - "openapi-typescript": "6.7.1" + "openapi-typescript": "6.7.1", + "ts-case-convert": "2.0.2", + "tsx": "4.4.0", + "typescript": "5.3.3" }, "files": [ "built" diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 53d5044d68..1e3ed99a9b 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@microsoft/api-extractor": "7.38.5", + "@misskey-dev/eslint-plugin": "^1.0.0", "@swc/jest": "0.2.29", "@types/jest": "29.5.11", "@types/node": "20.10.5", diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index d97646b7cc..75ab7d91b1 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -1,6 +1,14 @@ import { Endpoints as Gen } from './autogen/endpoint'; import { UserDetailed } from './autogen/models'; import { UsersShowRequest } from './autogen/entities'; +import { + SigninRequest, + SigninResponse, + SignupPendingRequest, + SignupPendingResponse, + SignupRequest, + SignupResponse, +} from './entities'; type Overwrite<T, U extends { [Key in keyof T]?: unknown }> = Omit< T, @@ -55,6 +63,21 @@ export type Endpoints = Overwrite< $default: UserDetailed; }; }; - } + }, + // api.jsonには載せないものなのでここで定義 + 'signup': { + req: SignupRequest; + res: SignupResponse; + }, + // api.jsonには載せないものなのでここで定義 + 'signup-pending': { + req: SignupPendingRequest; + res: SignupPendingResponse; + }, + // api.jsonには載せないものなのでここで定義 + 'signin': { + req: SigninRequest; + res: SigninResponse; + }, } > diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index ebd3f0025a..aed8e54fad 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1,6 +1,6 @@ /* - * version: 2023.12.0.beta3 - * generatedAt: 2024-01-02T12:58:03.874Z + * version: 2023.12.2 + * generatedAt: 2024-01-07T15:22:15.630Z */ import type { SwitchCaseResponseType } from '../api.js'; @@ -694,50 +694,6 @@ declare module '../api.js' { /** * No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:nsfw-user* - */ - request<E extends 'admin/nsfw-user', P extends Endpoints[E]['req']>( - endpoint: E, - params: P, - credential?: string | null, - ): Promise<SwitchCaseResponseType<E, P>>; - - /** - * No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:unnsfw-user* - */ - request<E extends 'admin/unnsfw-user', P extends Endpoints[E]['req']>( - endpoint: E, - params: P, - credential?: string | null, - ): Promise<SwitchCaseResponseType<E, P>>; - - /** - * No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:silence-user* - */ - request<E extends 'admin/silence-user', P extends Endpoints[E]['req']>( - endpoint: E, - params: P, - credential?: string | null, - ): Promise<SwitchCaseResponseType<E, P>>; - - /** - * No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:unsilence-user* - */ - request<E extends 'admin/unsilence-user', P extends Endpoints[E]['req']>( - endpoint: E, - params: P, - credential?: string | null, - ): Promise<SwitchCaseResponseType<E, P>>; - - /** - * No description provided. - * * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user* */ request<E extends 'admin/suspend-user', P extends Endpoints[E]['req']>( @@ -749,17 +705,6 @@ declare module '../api.js' { /** * No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:approve-user* - */ - request<E extends 'admin/approve-user', P extends Endpoints[E]['req']>( - endpoint: E, - params: P, - credential?: string | null, - ): Promise<SwitchCaseResponseType<E, P>>; - - /** - * No description provided. - * * **Credential required**: *Yes* / **Permission**: *write:admin:unsuspend-user* */ request<E extends 'admin/unsuspend-user', P extends Endpoints[E]['req']>( @@ -2322,6 +2267,18 @@ declare module '../api.js' { * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ + request<E extends 'i/export-clips', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* + */ request<E extends 'i/export-favorites', P extends Endpoints[E]['req']>( endpoint: E, params: P, @@ -2562,17 +2519,6 @@ declare module '../api.js' { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - request<E extends 'i/registry/get-unsecure', P extends Endpoints[E]['req']>( - endpoint: E, - params: P, - credential?: string | null, - ): Promise<SwitchCaseResponseType<E, P>>; - - /** - * No description provided. - * - * **Credential required**: *Yes* / **Permission**: *read:account* - */ request<E extends 'i/registry/get-detail', P extends Endpoints[E]['req']>( endpoint: E, params: P, @@ -3050,17 +2996,6 @@ declare module '../api.js' { /** * No description provided. * - * **Credential required**: *No* - */ - request<E extends 'notes/bubble-timeline', P extends Endpoints[E]['req']>( - endpoint: E, - params: P, - credential?: string | null, - ): Promise<SwitchCaseResponseType<E, P>>; - - /** - * No description provided. - * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'notes/hybrid-timeline', P extends Endpoints[E]['req']>( diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 94503fb89d..d22a48b3da 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,6 @@ /* - * version: 2023.12.0.beta3 - * generatedAt: 2024-01-02T12:58:03.868Z + * version: 2023.12.2 + * generatedAt: 2024-01-07T15:22:15.626Z */ import type { @@ -766,6 +766,7 @@ export type Endpoints = { 'i/export-following': { req: IExportFollowingRequest; res: EmptyResponse }; 'i/export-mute': { req: EmptyRequest; res: EmptyResponse }; 'i/export-notes': { req: EmptyRequest; res: EmptyResponse }; + 'i/export-clips': { req: EmptyRequest; res: EmptyResponse }; 'i/export-favorites': { req: EmptyRequest; res: EmptyResponse }; 'i/export-user-lists': { req: EmptyRequest; res: EmptyResponse }; 'i/export-antennas': { req: EmptyRequest; res: EmptyResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index f8038a49d1..b50bbe9042 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -1,6 +1,6 @@ /* - * version: 2023.12.0.beta3 - * generatedAt: 2024-01-02T12:58:03.865Z + * version: 2023.12.2 + * generatedAt: 2024-01-07T15:22:15.624Z */ import { operations } from './types.js'; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index b5481a69bc..90e2bf660b 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -1,6 +1,6 @@ /* - * version: 2023.12.0.beta3 - * generatedAt: 2024-01-02T12:58:03.862Z + * version: 2023.12.2 + * generatedAt: 2024-01-07T15:22:15.623Z */ import { components } from './types.js'; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 69bb68e8a3..0e068e5267 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -2,8 +2,8 @@ /* eslint @typescript-eslint/no-explicit-any: 0 */ /* - * version: 2023.12.0.beta3 - * generatedAt: 2024-01-02T12:58:03.687Z + * version: 2023.12.2 + * generatedAt: 2024-01-07T15:22:15.494Z */ /** @@ -2021,6 +2021,16 @@ export type paths = { */ post: operations['i/export-notes']; }; + '/i/export-clips': { + /** + * i/export-clips + * @description No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* + */ + post: operations['i/export-clips']; + }; '/i/export-favorites': { /** * i/export-favorites @@ -3919,13 +3929,14 @@ export type components = { * @example xxxxxxxxxx */ channelId?: string | null; - channel?: { + channel?: ({ id: string; name: string; color: string; isSensitive: boolean; allowRenoteToExternal: boolean; - } | null; + userId: string | null; + }) | null; localOnly?: boolean; reactionAcceptance: string | null; reactions: Record<string, never>; @@ -4533,6 +4544,9 @@ export type operations = { approvalRequiredForSignup: boolean; enableHcaptcha: boolean; hcaptchaSiteKey: string | null; + enableMcaptcha: boolean; + mcaptchaSiteKey: string | null; + mcaptchaInstanceUrl: string | null; enableRecaptcha: boolean; recaptchaSiteKey: string | null; enableTurnstile: boolean; @@ -4559,6 +4573,7 @@ export type operations = { preservedUsernames: string[]; bubbleInstances: string[]; hcaptchaSecretKey: string | null; + mcaptchaSecretKey: string | null; recaptchaSecretKey: string | null; turnstileSecretKey: string | null; sensitiveMediaDetection: string; @@ -4591,6 +4606,9 @@ export type operations = { enableActiveEmailValidation: boolean; enableVerifymailApi: boolean; verifymailAuthKey: string | null; + enableTruemailApi: boolean; + truemailInstance: string | null; + truemailAuthKey: string | null; enableChartsForRemoteUser: boolean; enableChartsForFederatedInstances: boolean; enableServerMachineStats: boolean; @@ -8593,6 +8611,10 @@ export type operations = { enableHcaptcha?: boolean; hcaptchaSiteKey?: string | null; hcaptchaSecretKey?: string | null; + enableMcaptcha?: boolean; + mcaptchaSiteKey?: string | null; + mcaptchaInstanceUrl?: string | null; + mcaptchaSecretKey?: string | null; enableRecaptcha?: boolean; recaptchaSiteKey?: string | null; recaptchaSecretKey?: string | null; @@ -8646,6 +8668,9 @@ export type operations = { enableActiveEmailValidation?: boolean; enableVerifymailApi?: boolean; verifymailAuthKey?: string | null; + enableTruemailApi?: boolean; + truemailInstance?: string | null; + truemailAuthKey?: string | null; enableChartsForRemoteUser?: boolean; enableChartsForFederatedInstances?: boolean; enableServerMachineStats?: boolean; @@ -16270,7 +16295,7 @@ export type operations = { content: { 'application/json': { /** @enum {string} */ - name: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted'; + name: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead'; }; }; }; @@ -16684,6 +16709,57 @@ export type operations = { }; }; /** + * i/export-clips + * @description No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* + */ + 'i/export-clips': { + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** * i/export-favorites * @description No description provided. * @@ -19272,6 +19348,9 @@ export type operations = { approvalRequiredForSignup: boolean; enableHcaptcha: boolean; hcaptchaSiteKey: string | null; + enableMcaptcha: boolean; + mcaptchaSiteKey: string | null; + mcaptchaInstanceUrl: string | null; enableRecaptcha: boolean; recaptchaSiteKey: string | null; enableTurnstile: boolean; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 2c490fa55e..e9ab61069e 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -1,5 +1,5 @@ import { ModerationLogPayloads } from './consts.js'; -import { Announcement, EmojiDetailed, Page, User, UserDetailed } from './autogen/models'; +import { Announcement, EmojiDetailed, MeDetailed, MeDetailedOnly, Page, User, UserDetailed } from './autogen/models'; export * from './autogen/entities'; export * from './autogen/models'; @@ -152,7 +152,7 @@ export type ServerStats = { } }; -export type ServerStatsLog = string[]; +export type ServerStatsLog = ServerStats[]; export type QueueStats = { deliver: { @@ -169,7 +169,7 @@ export type QueueStats = { }; }; -export type QueueStatsLog = string[]; +export type QueueStatsLog = QueueStats[]; export type EmojiAdded = { emoji: EmojiDetailed @@ -186,3 +186,38 @@ export type EmojiDeleted = { export type AnnouncementCreated = { announcement: Announcement; }; + +export type SignupRequest = { + username: string; + password: string; + host?: string; + invitationCode?: string; + emailAddress?: string; + 'hcaptcha-response'?: string | null; + 'g-recaptcha-response'?: string | null; + 'turnstile-response'?: string | null; +} + +export type SignupResponse = MeDetailed & { + token: string; +} + +export type SignupPendingRequest = { + code: string; +}; + +export type SignupPendingResponse = { + id: User['id'], + i: string, +}; + +export type SigninRequest = { + username: string; + password: string; + token?: string; +}; + +export type SigninResponse = { + id: User['id'], + i: string, +}; diff --git a/packages/shared/.eslintrc.js b/packages/shared/.eslintrc.js index b3c7626a39..58247877ae 100644 --- a/packages/shared/.eslintrc.js +++ b/packages/shared/.eslintrc.js @@ -1,118 +1,7 @@ module.exports = { root: true, - parser: '@typescript-eslint/parser', - plugins: [ - '@typescript-eslint', - 'import' - ], + ignorePatterns: ['**/.eslintrc.cjs'], extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:import/recommended', - 'plugin:import/typescript' + 'plugin:@misskey-dev/recommended', ], - rules: { - 'indent': ['warn', 'tab', { - 'SwitchCase': 1, - 'MemberExpression': 1, - 'flatTernaryExpressions': true, - 'ArrayExpression': 'first', - 'ObjectExpression': 'first', - }], - 'eol-last': ['error', 'always'], - 'semi': ['error', 'always'], - 'semi-spacing': ['error', { 'before': false, 'after': true }], - 'quotes': ['warn', 'single'], - 'comma-dangle': ['warn', 'always-multiline'], - 'comma-spacing': ['error', { 'before': false, 'after': true }], - 'array-bracket-spacing': ['error', 'never'], - 'keyword-spacing': ['error', { - 'before': true, - 'after': true, - }], - 'key-spacing': ['error', { - 'beforeColon': false, - 'afterColon': true, - }], - 'arrow-spacing': ['error', { - 'before': true, - 'after': true, - }], - 'brace-style': ['error', '1tbs', { - 'allowSingleLine': true, - }], - 'padded-blocks': ['error', 'never'], - /* TODO: path aliasを使わないとwarnする - 'no-restricted-imports': ['warn', { - 'patterns': [ - ] - }], - */ - 'eqeqeq': ['error', 'always', { 'null': 'ignore' }], - 'no-multi-spaces': ['error'], - 'no-var': ['error'], - 'prefer-arrow-callback': ['error'], - 'no-throw-literal': ['error'], - 'no-param-reassign': ['warn'], - 'no-constant-condition': ['warn'], - 'no-empty-pattern': ['warn'], - 'no-async-promise-executor': ['off'], - 'no-useless-escape': ['off'], - 'no-multiple-empty-lines': ['error', { 'max': 1 }], - 'no-control-regex': ['warn'], - 'no-empty': ['warn'], - 'no-inner-declarations': ['off'], - 'no-sparse-arrays': ['off'], - 'nonblock-statement-body-position': ['error', 'beside'], - 'object-curly-spacing': ['error', 'always'], - 'space-infix-ops': ['error'], - 'space-before-blocks': ['error', 'always'], - 'padding-line-between-statements': [ - 'error', - { 'blankLine': 'always', 'prev': 'function', 'next': '*' }, - { 'blankLine': 'always', 'prev': '*', 'next': 'function' }, - ], - "lines-between-class-members": "off", - /* typescript-eslint では enforce に対応してないっぽい - '@typescript-eslint/lines-between-class-members': ['error', { - enforce: [{ - blankLine: 'always', - prev: 'method', - next: '*', - }] - }], - */ - '@typescript-eslint/func-call-spacing': ['error', 'never'], - '@typescript-eslint/no-explicit-any': ['warn'], - '@typescript-eslint/no-unused-vars': ['warn'], - '@typescript-eslint/no-unnecessary-condition': ['warn'], - '@typescript-eslint/no-var-requires': ['warn'], - '@typescript-eslint/no-inferrable-types': ['warn'], - '@typescript-eslint/no-empty-function': ['off'], - '@typescript-eslint/no-non-null-assertion': ['warn'], - '@typescript-eslint/explicit-function-return-type': ['off'], - '@typescript-eslint/no-misused-promises': ['error', { - 'checksVoidReturn': false, - }], - '@typescript-eslint/consistent-type-imports': 'off', - '@typescript-eslint/prefer-nullish-coalescing': [ - 'warn', - ], - '@typescript-eslint/naming-convention': [ - 'error', - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "typeParameter", - "format": [] - } - ], - 'import/no-unresolved': ['off'], - 'import/no-default-export': ['warn'], - 'import/order': ['warn', { - 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], - }] - }, }; diff --git a/packages/sw/package.json b/packages/sw/package.json index c48efd6ea6..270bf09bd5 100644 --- a/packages/sw/package.json +++ b/packages/sw/package.json @@ -14,6 +14,7 @@ "misskey-js": "workspace:*" }, "devDependencies": { + "@misskey-dev/eslint-plugin": "^1.0.0", "@typescript-eslint/parser": "6.14.0", "@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67", "eslint": "8.56.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index badbc43294..0d888da367 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,15 +97,21 @@ importers: '@fastify/view': specifier: 8.2.0 version: 8.2.0 + '@misskey-dev/sharp-read-bmp': + specifier: ^1.1.1 + version: 1.1.1 + '@misskey-dev/summaly': + specifier: ^5.0.3 + version: 5.0.3 '@nestjs/common': specifier: 10.2.10 version: 10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1) '@nestjs/core': specifier: 10.2.10 - version: 10.2.10(@nestjs/common@10.2.10)(reflect-metadata@0.1.14)(rxjs@7.8.1) + version: 10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.0)(reflect-metadata@0.1.14)(rxjs@7.8.1) '@nestjs/testing': specifier: 10.2.10 - version: 10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10) + version: 10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/platform-express@10.3.0) '@peertube/http-signature': specifier: 1.7.0 version: 1.7.0 @@ -355,9 +361,6 @@ importers: sharp: specifier: 0.32.6 version: 0.32.6 - sharp-read-bmp: - specifier: github:misskey-dev/sharp-read-bmp - version: github.com/misskey-dev/sharp-read-bmp/02d9dc189fa7df0c4bea09330be26741772dac01 slacc: specifier: 0.0.10 version: 0.0.10 @@ -367,9 +370,6 @@ importers: stringz: specifier: 2.1.0 version: 2.1.0 - summaly: - specifier: github:misskey-dev/summaly - version: github.com/misskey-dev/summaly/d2a3e07205c3c9769bc5a7b42031c8884b5a25c8 systeminformation: specifier: 5.21.20 version: 5.21.20 @@ -495,6 +495,12 @@ importers: '@jest/globals': specifier: 29.7.0 version: 29.7.0 + '@misskey-dev/eslint-plugin': + specifier: ^1.0.0 + version: 1.0.0(@typescript-eslint/eslint-plugin@6.14.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) + '@nestjs/platform-express': + specifier: ^10.3.0 + version: 10.3.0(@nestjs/common@10.2.10)(@nestjs/core@10.2.10) '@simplewebauthn/typescript-types': specifier: 8.3.4 version: 8.3.4 @@ -642,6 +648,9 @@ importers: execa: specifier: 8.0.1 version: 8.0.1 + fkill: + specifier: ^9.0.0 + version: 9.0.0 jest: specifier: 29.7.0 version: 29.7.0(@types/node@20.10.5) @@ -651,6 +660,9 @@ importers: nodemon: specifier: 3.0.2 version: 3.0.2 + pid-port: + specifier: ^1.0.0 + version: 1.0.0 simple-oauth2: specifier: 5.0.0 version: 5.0.0 @@ -663,6 +675,12 @@ importers: '@github/webauthn-json': specifier: 2.1.1 version: 2.1.1 + '@mcaptcha/vanilla-glue': + specifier: 0.1.0-alpha-3 + version: 0.1.0-alpha-3 + '@misskey-dev/browser-image-resizer': + specifier: 2.2.1-misskey.10 + version: 2.2.1-misskey.10 '@phosphor-icons/web': specifier: ^2.0.3 version: 2.0.3 @@ -685,11 +703,11 @@ importers: specifier: 15.0.0 version: 15.0.0 '@vitejs/plugin-vue': - specifier: 4.5.2 - version: 4.5.2(vite@5.0.10)(vue@3.3.12) + specifier: 5.0.2 + version: 5.0.2(vite@5.0.10)(vue@3.4.3) '@vue/compiler-sfc': - specifier: 3.3.12 - version: 3.3.12 + specifier: 3.4.3 + version: 3.4.3 aiscript-vscode: specifier: github:aiscript-dev/aiscript-vscode#v0.0.6 version: github.com/aiscript-dev/aiscript-vscode/b5a8aa0ad927831a0b867d1c183460a14e6c48cd @@ -699,9 +717,6 @@ importers: broadcast-channel: specifier: 7.0.0 version: 7.0.0 - browser-image-resizer: - specifier: github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3 - version: github.com/misskey-dev/browser-image-resizer/0227e860621e55cbed0aabe6dc601096a7748c4a buraha: specifier: 0.0.1 version: 0.0.1 @@ -744,9 +759,6 @@ importers: eventemitter3: specifier: 5.0.1 version: 5.0.1 - gsap: - specifier: 3.12.4 - version: 3.12.4 idb-keyval: specifier: 6.2.1 version: 6.2.1 @@ -815,17 +827,23 @@ importers: version: 9.0.1 v-code-diff: specifier: 1.7.2 - version: 1.7.2(vue@3.3.12) + version: 1.7.2(vue@3.4.3) vite: specifier: 5.0.10 version: 5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.26.0) vue: - specifier: 3.3.12 - version: 3.3.12(typescript@5.3.3) + specifier: 3.4.3 + version: 3.4.3(typescript@5.3.3) vuedraggable: specifier: next - version: 4.1.0(vue@3.3.12) + version: 4.1.0(vue@3.4.3) devDependencies: + '@misskey-dev/eslint-plugin': + specifier: ^1.0.0 + version: 1.0.0(@typescript-eslint/eslint-plugin@6.14.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) + '@misskey-dev/summaly': + specifier: ^5.0.3 + version: 5.0.3 '@storybook/addon-actions': specifier: 7.6.5 version: 7.6.5 @@ -876,13 +894,13 @@ importers: version: 7.6.5 '@storybook/vue3': specifier: 7.6.5 - version: 7.6.5(@vue/compiler-core@3.3.12)(vue@3.3.12) + version: 7.6.5(@vue/compiler-core@3.3.12)(vue@3.4.3) '@storybook/vue3-vite': specifier: 7.6.5 - version: 7.6.5(@vue/compiler-core@3.3.12)(typescript@5.3.3)(vite@5.0.10)(vue@3.3.12) + version: 7.6.5(@vue/compiler-core@3.3.12)(typescript@5.3.3)(vite@5.0.10)(vue@3.4.3) '@testing-library/vue': specifier: 8.0.1 - version: 8.0.1(@vue/compiler-sfc@3.3.12)(vue@3.3.12) + version: 8.0.1(@vue/compiler-sfc@3.4.3)(vue@3.4.3) '@types/escape-regexp': specifier: 0.0.3 version: 0.0.3 @@ -926,8 +944,8 @@ importers: specifier: 0.34.6 version: 0.34.6(vitest@0.34.6) '@vue/runtime-core': - specifier: 3.3.12 - version: 3.3.12 + specifier: 3.4.3 + version: 3.4.3 acorn: specifier: 8.11.2 version: 8.11.2 @@ -985,9 +1003,6 @@ importers: storybook-addon-misskey-theme: specifier: github:misskey-dev/storybook-addon-misskey-theme version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.6.5)(@storybook/components@7.6.5)(@storybook/core-events@7.6.5)(@storybook/manager-api@7.6.5)(@storybook/preview-api@7.6.5)(@storybook/theming@7.6.5)(@storybook/types@7.6.5)(react-dom@18.2.0)(react@18.2.0) - summaly: - specifier: github:misskey-dev/summaly - version: github.com/misskey-dev/summaly/d2a3e07205c3c9769bc5a7b42031c8884b5a25c8 vite-plugin-turbosnap: specifier: 1.0.3 version: 1.0.3 @@ -1001,8 +1016,8 @@ importers: specifier: 9.3.2 version: 9.3.2(eslint@8.56.0) vue-tsc: - specifier: 1.8.25 - version: 1.8.25(typescript@5.3.3) + specifier: 1.8.27 + version: 1.8.27(typescript@5.3.3) packages/megalodon: dependencies: @@ -1113,6 +1128,9 @@ importers: '@microsoft/api-extractor': specifier: 7.38.5 version: 7.38.5(@types/node@20.10.5) + '@misskey-dev/eslint-plugin': + specifier: ^1.0.0 + version: 1.0.0(@typescript-eslint/eslint-plugin@6.14.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) '@swc/jest': specifier: 0.2.29 version: 0.2.29(@swc/core@1.3.100) @@ -1161,6 +1179,9 @@ importers: '@apidevtools/swagger-parser': specifier: 10.1.0 version: 10.1.0(openapi-types@12.1.3) + '@misskey-dev/eslint-plugin': + specifier: ^1.0.0 + version: 1.0.0(@typescript-eslint/eslint-plugin@6.11.0)(@typescript-eslint/parser@6.11.0)(eslint-plugin-import@2.29.1)(eslint@8.53.0) '@types/node': specifier: 20.9.1 version: 20.9.1 @@ -1201,6 +1222,9 @@ importers: specifier: workspace:* version: link:../misskey-js devDependencies: + '@misskey-dev/eslint-plugin': + specifier: ^1.0.0 + version: 1.0.0(@typescript-eslint/eslint-plugin@6.14.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) '@typescript-eslint/parser': specifier: 6.14.0 version: 6.14.0(eslint@8.56.0)(typescript@5.3.3) @@ -4804,7 +4828,6 @@ packages: /@lukeed/csprng@1.0.1: resolution: {integrity: sha512-uSvJdwQU5nK+Vdf6zxcWAY2A8r7uqe+gePwLWzJ+fsQehq18pc0I2hJKwypZ2aLM90+Er9u1xn4iLJPZ+xlL4g==} engines: {node: '>=8'} - dev: false /@lukeed/ms@2.0.1: resolution: {integrity: sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==} @@ -4829,6 +4852,16 @@ packages: - supports-color dev: false + /@mcaptcha/core-glue@0.1.0-alpha-5: + resolution: {integrity: sha512-16qWm5O5X0Y9LXULULaAks8Vf9FNlUUBcR5KDt49aWhFhG5++JzxNmCwQM9EJSHNU7y0U+FdyAWcGmjfKlkRLA==} + dev: false + + /@mcaptcha/vanilla-glue@0.1.0-alpha-3: + resolution: {integrity: sha512-GT6TJBgmViGXcXiT5VOr+h/6iOnThSlZuCoOWncubyTZU9R3cgU5vWPkF7G6Ob6ee2CBe3yqBxxk24CFVGTVXw==} + dependencies: + '@mcaptcha/core-glue': 0.1.0-alpha-5 + dev: false + /@mdx-js/react@2.3.0(react@18.2.0): resolution: {integrity: sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==} peerDependencies: @@ -4882,6 +4915,58 @@ packages: resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} dev: true + /@misskey-dev/browser-image-resizer@2.2.1-misskey.10: + resolution: {integrity: sha512-Spjiwa8brffhz4FiYrZ8VoPRyPPRzcdaIzLVb8oMnD9YGU3uzcX/CcZ08okFhrUR/N6IlQM86r5dNH/yY5Uyjg==} + dev: false + + /@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@6.11.0)(@typescript-eslint/parser@6.11.0)(eslint-plugin-import@2.29.1)(eslint@8.53.0): + resolution: {integrity: sha512-dh6UbcrNDVg5DD8k8Qh4ab30OPpuEYIlJCqaBV/lkIV8wNN/AfCJ2V7iTP8V8KjryM4t+sf5IqzQLQnT0mWI4A==} + peerDependencies: + '@typescript-eslint/eslint-plugin': '>= 6' + '@typescript-eslint/parser': '>= 6' + eslint: '>= 3' + eslint-plugin-import: '>= 2' + dependencies: + '@typescript-eslint/eslint-plugin': 6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3) + eslint: 8.53.0 + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.11.0)(eslint@8.53.0) + dev: true + + /@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@6.14.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0): + resolution: {integrity: sha512-dh6UbcrNDVg5DD8k8Qh4ab30OPpuEYIlJCqaBV/lkIV8wNN/AfCJ2V7iTP8V8KjryM4t+sf5IqzQLQnT0mWI4A==} + peerDependencies: + '@typescript-eslint/eslint-plugin': '>= 6' + '@typescript-eslint/parser': '>= 6' + eslint: '>= 3' + eslint-plugin-import: '>= 2' + dependencies: + '@typescript-eslint/eslint-plugin': 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.56.0) + dev: true + + /@misskey-dev/sharp-read-bmp@1.1.1: + resolution: {integrity: sha512-X52BQYL/I9mafypQ+wBhst+BUlYiPWnHhKGcF6ybcYSLl+zhcV0q5mezIXHozhM0Sv0A7xCdrWmR7TCNxHLrtQ==} + dependencies: + decode-bmp: 0.2.1 + decode-ico: 0.4.1 + sharp: 0.32.6 + dev: false + + /@misskey-dev/summaly@5.0.3: + resolution: {integrity: sha512-jVkuLEDrq2FaeHL8VY51LTqB6j0Jv5L7s0nmKGKMnE0jPBpSj6flswnZgntGmz5mbdCj47utEqu8FY43kH7PVg==} + dependencies: + cheerio: 1.0.0-rc.12 + escape-regexp: 0.0.1 + got: 12.6.1 + html-entities: 2.3.2 + iconv-lite: 0.6.3 + jschardet: 3.0.0 + private-ip: 2.3.3 + trace-redirect: 1.0.6 + /@mole-inc/bin-wrapper@8.0.1: resolution: {integrity: sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4994,9 +5079,8 @@ packages: rxjs: 7.8.1 tslib: 2.6.2 uid: 2.0.2 - dev: false - /@nestjs/core@10.2.10(@nestjs/common@10.2.10)(reflect-metadata@0.1.14)(rxjs@7.8.1): + /@nestjs/core@10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.0)(reflect-metadata@0.1.14)(rxjs@7.8.1): resolution: {integrity: sha512-+ckOI6BPi2ZMHikT9MCG4ctHDc4OnjhoIytrn7f2AYMMXI4bnutJhqyQKc30VDka5x3Wq6QAD57pgSP7y+JjJg==} requiresBuild: true peerDependencies: @@ -5015,6 +5099,7 @@ packages: optional: true dependencies: '@nestjs/common': 10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1) + '@nestjs/platform-express': 10.3.0(@nestjs/common@10.2.10)(@nestjs/core@10.2.10) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -5025,9 +5110,24 @@ packages: uid: 2.0.2 transitivePeerDependencies: - encoding - dev: false - /@nestjs/testing@10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10): + /@nestjs/platform-express@10.3.0(@nestjs/common@10.2.10)(@nestjs/core@10.2.10): + resolution: {integrity: sha512-E4hUW48bYv8OHbP9XQg6deefmXb0pDSSuE38SdhA0mJ37zGY7C5EqqBUdlQk4ttfD+OdnbIgJ1zOokT6dd2d7A==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + dependencies: + '@nestjs/common': 10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1) + '@nestjs/core': 10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.0)(reflect-metadata@0.1.14)(rxjs@7.8.1) + body-parser: 1.20.2 + cors: 2.8.5 + express: 4.18.2 + multer: 1.4.4-lts.1 + tslib: 2.6.2 + transitivePeerDependencies: + - supports-color + + /@nestjs/testing@10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/platform-express@10.3.0): resolution: {integrity: sha512-IVLUnPz/+fkBtPATYfqTIP+phN9yjkXejmj+JyhmcfPJZpxBmD1i9VSMqa4u54l37j0xkGPscQ0IXpbhqMYUKw==} peerDependencies: '@nestjs/common': ^10.0.0 @@ -5041,7 +5141,8 @@ packages: optional: true dependencies: '@nestjs/common': 10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1) - '@nestjs/core': 10.2.10(@nestjs/common@10.2.10)(reflect-metadata@0.1.14)(rxjs@7.8.1) + '@nestjs/core': 10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.0)(reflect-metadata@0.1.14)(rxjs@7.8.1) + '@nestjs/platform-express': 10.3.0(@nestjs/common@10.2.10)(@nestjs/core@10.2.10) tslib: 2.6.2 dev: false @@ -5093,7 +5194,6 @@ packages: node-fetch: 2.7.0 transitivePeerDependencies: - encoding - dev: false /@one-ini/wasm@0.1.1: resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} @@ -7182,7 +7282,7 @@ packages: file-system-cache: 2.3.0 dev: true - /@storybook/vue3-vite@7.6.5(@vue/compiler-core@3.3.12)(typescript@5.3.3)(vite@5.0.10)(vue@3.3.12): + /@storybook/vue3-vite@7.6.5(@vue/compiler-core@3.3.12)(typescript@5.3.3)(vite@5.0.10)(vue@3.4.3): resolution: {integrity: sha512-7wUCq2Lrjlekftd5ha3hG0GSGbbzuc370cKkBqSmwFuOfI38z5+VeYt7nDtAlncxcpVSH7DejTGRuKTlC7NyYg==} engines: {node: ^14.18 || >=16} peerDependencies: @@ -7190,11 +7290,11 @@ packages: dependencies: '@storybook/builder-vite': 7.6.5(typescript@5.3.3)(vite@5.0.10) '@storybook/core-server': 7.6.5 - '@storybook/vue3': 7.6.5(@vue/compiler-core@3.3.12)(vue@3.3.12) - '@vitejs/plugin-vue': 4.5.2(vite@5.0.10)(vue@3.3.12) + '@storybook/vue3': 7.6.5(@vue/compiler-core@3.3.12)(vue@3.4.3) + '@vitejs/plugin-vue': 4.5.2(vite@5.0.10)(vue@3.4.3) magic-string: 0.30.5 vite: 5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.26.0) - vue-docgen-api: 4.64.1(vue@3.3.12) + vue-docgen-api: 4.64.1(vue@3.4.3) transitivePeerDependencies: - '@preact/preset-vite' - '@vue/compiler-core' @@ -7207,7 +7307,7 @@ packages: - vue dev: true - /@storybook/vue3@7.6.5(@vue/compiler-core@3.3.12)(vue@3.3.12): + /@storybook/vue3@7.6.5(@vue/compiler-core@3.3.12)(vue@3.4.3): resolution: {integrity: sha512-tv/9rVc3XXDOJu5hfZtKhrhM8x4GTLKon62Rmaxlq06weqkGlfBi/V/g1EZ7OE71Pi+woKS/TX7p9qbRrvgahg==} engines: {node: '>=16.0.0'} peerDependencies: @@ -7223,7 +7323,7 @@ packages: lodash: 4.17.21 ts-dedent: 2.2.0 type-fest: 2.19.0 - vue: 3.3.12(typescript@5.3.3) + vue: 3.4.3(typescript@5.3.3) vue-component-type-helpers: 1.8.27 transitivePeerDependencies: - encoding @@ -7569,7 +7669,7 @@ packages: '@testing-library/dom': 9.2.0 dev: true - /@testing-library/vue@8.0.1(@vue/compiler-sfc@3.3.12)(vue@3.3.12): + /@testing-library/vue@8.0.1(@vue/compiler-sfc@3.4.3)(vue@3.4.3): resolution: {integrity: sha512-l51ZEpjTQ6glq3wM+asQ1GbKJMGcxwgHEygETx0aCRN4TjFEGvMZy4YdWKs/y7bu4bmLrxcxhbEPP7iPSW/2OQ==} engines: {node: '>=14'} peerDependencies: @@ -7578,9 +7678,9 @@ packages: dependencies: '@babel/runtime': 7.23.2 '@testing-library/dom': 9.3.3 - '@vue/compiler-sfc': 3.3.12 - '@vue/test-utils': 2.4.1(vue@3.3.12) - vue: 3.3.12(typescript@5.3.3) + '@vue/compiler-sfc': 3.4.3 + '@vue/test-utils': 2.4.1(vue@3.4.3) + vue: 3.4.3(typescript@5.3.3) transitivePeerDependencies: - '@vue/server-renderer' dev: true @@ -8645,7 +8745,7 @@ packages: - supports-color dev: true - /@vitejs/plugin-vue@4.5.2(vite@5.0.10)(vue@3.3.12): + /@vitejs/plugin-vue@4.5.2(vite@5.0.10)(vue@3.4.3): resolution: {integrity: sha512-UGR3DlzLi/SaVBPX0cnSyE37vqxU3O6chn8l0HJNzQzDia6/Au2A4xKv+iIJW8w2daf80G7TYHhi1pAUjdZ0bQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -8653,7 +8753,19 @@ packages: vue: ^3.2.25 dependencies: vite: 5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.26.0) - vue: 3.3.12(typescript@5.3.3) + vue: 3.4.3(typescript@5.3.3) + dev: true + + /@vitejs/plugin-vue@5.0.2(vite@5.0.10)(vue@3.4.3): + resolution: {integrity: sha512-kEjJHrLb5ePBvjD0SPZwJlw1QTRcjjCA9sB5VyfonoXVBxTS7TMnqL6EkLt1Eu61RDeiuZ/WN9Hf6PxXhPI2uA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 + vue: ^3.2.25 + dependencies: + vite: 5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.26.0) + vue: 3.4.3(typescript@5.3.3) + dev: false /@vitest/coverage-v8@0.34.6(vitest@0.34.6): resolution: {integrity: sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==} @@ -8740,21 +8852,32 @@ packages: '@vue/shared': 3.3.12 estree-walker: 2.0.2 source-map-js: 1.0.2 + dev: true /@vue/compiler-core@3.3.8: resolution: {integrity: sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==} dependencies: - '@babel/parser': 7.23.4 + '@babel/parser': 7.23.6 '@vue/shared': 3.3.8 estree-walker: 2.0.2 source-map-js: 1.0.2 dev: true + /@vue/compiler-core@3.4.3: + resolution: {integrity: sha512-u8jzgFg0EDtSrb/hG53Wwh1bAOQFtc1ZCegBpA/glyvTlgHl+tq13o1zvRfLbegYUw/E4mSTGOiCnAJ9SJ+lsg==} + dependencies: + '@babel/parser': 7.23.6 + '@vue/shared': 3.4.3 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.0.2 + /@vue/compiler-dom@3.3.12: resolution: {integrity: sha512-RdJU9oEYaoPKUdGXCy0l+i4clesdDeLmbvRlszoc9iagsnBnMmQtYfCPVQ5BHB6o7K4SCucDdJM2Dh3oXB0D6g==} dependencies: '@vue/compiler-core': 3.3.12 '@vue/shared': 3.3.12 + dev: true /@vue/compiler-dom@3.3.8: resolution: {integrity: sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==} @@ -8763,28 +8886,33 @@ packages: '@vue/shared': 3.3.8 dev: true - /@vue/compiler-sfc@3.3.12: - resolution: {integrity: sha512-yy5b9e7b79dsGbMmglCe/YnhCQgBkHO7Uf6JfjWPSf2/5XH+MKn18LhzhHyxbHdJgnA4lZCqtXzLaJz8Pd8lMw==} + /@vue/compiler-dom@3.4.3: + resolution: {integrity: sha512-oGF1E9/htI6JWj/lTJgr6UgxNCtNHbM6xKVreBWeZL9QhRGABRVoWGAzxmtBfSOd+w0Zi5BY0Es/tlJrN6WgEg==} + dependencies: + '@vue/compiler-core': 3.4.3 + '@vue/shared': 3.4.3 + + /@vue/compiler-sfc@3.4.3: + resolution: {integrity: sha512-NuJqb5is9I4uzv316VRUDYgIlPZCG8D+ARt5P4t5UDShIHKL25J3TGZAUryY/Aiy0DsY7srJnZL5ryB6DD63Zw==} dependencies: '@babel/parser': 7.23.6 - '@vue/compiler-core': 3.3.12 - '@vue/compiler-dom': 3.3.12 - '@vue/compiler-ssr': 3.3.12 - '@vue/reactivity-transform': 3.3.12 - '@vue/shared': 3.3.12 + '@vue/compiler-core': 3.4.3 + '@vue/compiler-dom': 3.4.3 + '@vue/compiler-ssr': 3.4.3 + '@vue/shared': 3.4.3 estree-walker: 2.0.2 magic-string: 0.30.5 postcss: 8.4.32 source-map-js: 1.0.2 - /@vue/compiler-ssr@3.3.12: - resolution: {integrity: sha512-adCiMJPznfWcQyk/9HSuXGja859IaMV+b8UNSVzDatqv7h0PvT9BEeS22+gjkWofDiSg5d78/ZLls3sLA+cn3A==} + /@vue/compiler-ssr@3.4.3: + resolution: {integrity: sha512-wnYQtMBkeFSxgSSQbYGQeXPhQacQiog2c6AlvMldQH6DB+gSXK/0F6DVXAJfEiuBSgBhUc8dwrrG5JQcqwalsA==} dependencies: - '@vue/compiler-dom': 3.3.12 - '@vue/shared': 3.3.12 + '@vue/compiler-dom': 3.4.3 + '@vue/shared': 3.4.3 - /@vue/language-core@1.8.25(typescript@5.3.3): - resolution: {integrity: sha512-NJk/5DnAZlpvXX8BdWmHI45bWGLViUaS3R/RMrmFSvFMSbJKuEODpM4kR0F0Ofv5SFzCWuNiMhxameWpVdQsnA==} + /@vue/language-core@1.8.27(typescript@5.3.3): + resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -8793,8 +8921,8 @@ packages: dependencies: '@volar/language-core': 1.11.1 '@volar/source-map': 1.11.1 - '@vue/compiler-dom': 3.3.8 - '@vue/shared': 3.3.8 + '@vue/compiler-dom': 3.3.12 + '@vue/shared': 3.3.12 computeds: 0.0.1 minimatch: 9.0.3 muggle-string: 0.3.1 @@ -8803,50 +8931,45 @@ packages: vue-template-compiler: 2.7.14 dev: true - /@vue/reactivity-transform@3.3.12: - resolution: {integrity: sha512-g5TijmML7FyKkLt6QnpqNmA4KD7K/T5SbXa88Bhq+hydNQEkzA8veVXWAQuNqg9rjaFYD0rPf0a9NofKA0ENgg==} + /@vue/reactivity@3.4.3: + resolution: {integrity: sha512-q5f9HLDU+5aBKizXHAx0w4whkIANs1Muiq9R5YXm0HtorSlflqv9u/ohaMxuuhHWCji4xqpQ1eL04WvmAmGnFg==} dependencies: - '@babel/parser': 7.23.6 - '@vue/compiler-core': 3.3.12 - '@vue/shared': 3.3.12 - estree-walker: 2.0.2 - magic-string: 0.30.5 - - /@vue/reactivity@3.3.12: - resolution: {integrity: sha512-vOJORzO8DlIx88cgTnMLIf2GlLYpoXAKsuoQsK6SGdaqODjxO129pVPTd2s/N/Mb6KKZEFIHIEwWGmtN4YPs+g==} - dependencies: - '@vue/shared': 3.3.12 + '@vue/shared': 3.4.3 - /@vue/runtime-core@3.3.12: - resolution: {integrity: sha512-5iL4w7MZrSGKEZU2wFAYhDZdZmgn+s//73EfgDXW1M+ZUOl36md7tlWp1QFK/ladiq4FvQ82shVjo0KiPDPr0A==} + /@vue/runtime-core@3.4.3: + resolution: {integrity: sha512-C1r6QhB1qY7D591RCSFhMULyzL9CuyrGc+3PpB0h7dU4Qqw6GNyo4BNFjHZVvsWncrUlKX3DIKg0Y7rNNr06NQ==} dependencies: - '@vue/reactivity': 3.3.12 - '@vue/shared': 3.3.12 + '@vue/reactivity': 3.4.3 + '@vue/shared': 3.4.3 - /@vue/runtime-dom@3.3.12: - resolution: {integrity: sha512-8mMzqiIdl+IYa/OXwKwk6/4ebLq7cYV1pUcwCSwBK2KerUa6cwGosen5xrCL9f8o2DJ9TfPFwbPEvH7OXzUpoA==} + /@vue/runtime-dom@3.4.3: + resolution: {integrity: sha512-wrsprg7An5Ec+EhPngWdPuzkp0BEUxAKaQtN9dPU/iZctPyD9aaXmVtehPJerdQxQale6gEnhpnfywNw3zOv2A==} dependencies: - '@vue/runtime-core': 3.3.12 - '@vue/shared': 3.3.12 + '@vue/runtime-core': 3.4.3 + '@vue/shared': 3.4.3 csstype: 3.1.3 - /@vue/server-renderer@3.3.12(vue@3.3.12): - resolution: {integrity: sha512-OZ0IEK5TU5GXb5J8/wSplyxvGGdIcwEmS8EIO302Vz8K6fGSgSJTU54X0Sb6PaefzZdiN3vHsLXO8XIeF8crQQ==} + /@vue/server-renderer@3.4.3(vue@3.4.3): + resolution: {integrity: sha512-BUxt8oVGMKKsqSkM1uU3d3Houyfy4WAc2SpSQRebNd+XJGATVkW/rO129jkyL+kpB/2VRKzE63zwf5RtJ3XuZw==} peerDependencies: - vue: 3.3.12 + vue: 3.4.3 dependencies: - '@vue/compiler-ssr': 3.3.12 - '@vue/shared': 3.3.12 - vue: 3.3.12(typescript@5.3.3) + '@vue/compiler-ssr': 3.4.3 + '@vue/shared': 3.4.3 + vue: 3.4.3(typescript@5.3.3) /@vue/shared@3.3.12: resolution: {integrity: sha512-6p0Yin0pclvnER7BLNOQuod9Z+cxSYh8pSh7CzHnWNjAIP6zrTlCdHRvSCb1aYEx6i3Q3kvfuWU7nG16CgG1ag==} + dev: true /@vue/shared@3.3.8: resolution: {integrity: sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==} dev: true - /@vue/test-utils@2.4.1(vue@3.3.12): + /@vue/shared@3.4.3: + resolution: {integrity: sha512-rIwlkkP1n4uKrRzivAKPZIEkHiuwY5mmhMJ2nZKCBLz8lTUlE73rQh4n1OnnMurXt1vcUNyH4ZPfdh8QweTjpQ==} + + /@vue/test-utils@2.4.1(vue@3.4.3): resolution: {integrity: sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==} peerDependencies: '@vue/server-renderer': ^3.0.1 @@ -8856,7 +8979,7 @@ packages: optional: true dependencies: js-beautify: 1.14.9 - vue: 3.3.12(typescript@5.3.3) + vue: 3.4.3(typescript@5.3.3) vue-component-type-helpers: 1.8.4 dev: true @@ -8993,6 +9116,14 @@ packages: clean-stack: 2.2.0 indent-string: 4.0.0 + /aggregate-error@5.0.0: + resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==} + engines: {node: '>=18'} + dependencies: + clean-stack: 5.2.0 + indent-string: 5.0.0 + dev: true + /ajv-draft-04@1.0.0(ajv@8.12.0): resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -9097,7 +9228,6 @@ packages: /append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} - dev: false /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} @@ -9669,7 +9799,6 @@ packages: unpipe: 1.0.0 transitivePeerDependencies: - supports-color - dev: false /boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -9822,7 +9951,6 @@ packages: engines: {node: '>=10.16.0'} dependencies: streamsearch: 1.1.0 - dev: false /bytes@3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} @@ -10159,6 +10287,13 @@ packages: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} + /clean-stack@5.2.0: + resolution: {integrity: sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==} + engines: {node: '>=14.16'} + dependencies: + escape-string-regexp: 5.0.0 + dev: true + /cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -10408,7 +10543,6 @@ packages: inherits: 2.0.4 readable-stream: 2.3.8 typedarray: 0.0.6 - dev: true /concat-stream@2.0.0: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} @@ -10429,7 +10563,6 @@ packages: /consola@2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} - dev: false /console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -10494,6 +10627,13 @@ packages: /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + /crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -11499,7 +11639,6 @@ packages: /escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - dev: false /escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} @@ -11546,6 +11685,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.11.0)(eslint-import-resolver-node@0.3.9)(eslint@8.53.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3) + debug: 3.2.7(supports-color@8.1.1) + eslint: 8.53.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} @@ -11575,6 +11743,41 @@ packages: - supports-color dev: true + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.11.0)(eslint@8.53.0): + resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3) + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7(supports-color@8.1.1) + doctrine: 2.1.0 + eslint: 8.53.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.11.0)(eslint-import-resolver-node@0.3.9)(eslint@8.53.0) + hasown: 2.0.0 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.56.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} @@ -11920,6 +12123,21 @@ packages: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + /execa@6.1.0: + resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 3.0.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + dev: true + /execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -12121,7 +12339,6 @@ packages: /fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - dev: false /fast-uri@2.2.0: resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==} @@ -12359,6 +12576,18 @@ packages: semver-regex: 4.0.5 dev: false + /fkill@9.0.0: + resolution: {integrity: sha512-MdYSsbdCaIRjzo5edthZtWmEZVMfr1qrtYZUHIdO3swCE+CoZA8S5l0s4jDsYlTa9ZiXv0pTgpzE7s4N8NeUOA==} + engines: {node: '>=18'} + dependencies: + aggregate-error: 5.0.0 + execa: 8.0.1 + pid-port: 1.0.0 + process-exists: 5.0.0 + ps-list: 8.1.1 + taskkill: 5.0.0 + dev: true + /flat-cache@3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -12844,10 +13073,6 @@ packages: engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} dev: true - /gsap@3.12.4: - resolution: {integrity: sha512-1ByAq8dD0W4aBZ/JArgaQvc0gyUfkGkP8mgAQa0qZGdpOKlSOhOf+WNXjoLimKaKG3Z4Iu6DKZtnyszqQeyqWQ==} - dev: false - /gunzip-maybe@1.4.2: resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} hasBin: true @@ -13126,6 +13351,11 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + /human-signals@3.0.1: + resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} + engines: {node: '>=12.20.0'} + dev: true + /human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -13190,6 +13420,11 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + /indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + dev: true + /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: @@ -13717,7 +13952,6 @@ packages: /iterare@1.2.1: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} - dev: false /jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} @@ -15113,7 +15347,6 @@ packages: hasBin: true dependencies: minimist: 1.2.8 - dev: true /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} @@ -15237,6 +15470,18 @@ packages: resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} dev: true + /multer@1.4.4-lts.1: + resolution: {integrity: sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==} + engines: {node: '>= 6.0.0'} + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 1.6.2 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + /mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} dev: true @@ -15334,10 +15579,6 @@ packages: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} dev: false - /node-addon-api@5.1.0: - resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} - dev: false - /node-addon-api@6.1.0: resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} @@ -15978,7 +16219,6 @@ packages: /path-to-regexp@3.2.0: resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} - dev: false /path-to-regexp@6.2.1: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} @@ -16115,6 +16355,13 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + /pid-port@1.0.0: + resolution: {integrity: sha512-LSNBeKChRPA4Xlrs6+zV588G1hSrFvANtPV5rt/5MPfSPK3V9XPWxx1d29svsrOjngT9ifLisXWCLS7DvO9ZhQ==} + engines: {node: '>=18'} + dependencies: + execa: 8.0.1 + dev: true + /pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -16680,6 +16927,13 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: false + /process-exists@5.0.0: + resolution: {integrity: sha512-6QPRh5fyHD8MaXr4GYML8K/YY0Sq5dKHGIOrAKS3cYpHQdmygFCcijIu1dVoNKAZ0TWAMoeh8KDK9dF8auBkJA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + ps-list: 8.1.1 + dev: true + /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -16751,6 +17005,11 @@ packages: /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + /ps-list@8.1.1: + resolution: {integrity: sha512-OPS9kEJYVmiO48u/B9qneqhkMvgCxT+Tm28VCEJpheTpl8cJ0ffZRRNgS5mrQRTrX5yRTpaJ+hRDeefXYmmorQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /ps-tree@1.2.0: resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} engines: {node: '>= 0.10'} @@ -17011,7 +17270,6 @@ packages: http-errors: 2.0.0 iconv-lite: 0.4.24 unpipe: 1.0.0 - dev: false /rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} @@ -17326,7 +17584,6 @@ packages: /reflect-metadata@0.1.14: resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} - dev: false /regenerate-unicode-properties@10.1.0: resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} @@ -17770,21 +18027,6 @@ packages: kind-of: 6.0.3 dev: true - /sharp@0.31.3: - resolution: {integrity: sha512-XcR4+FCLBFKw1bdB+GEhnUNXNXvnt0tDo4WsBsraKymuo/IAuPuCBVAL2wIkUw2r/dwFW5Q5+g66Kwl2dgDFVg==} - engines: {node: '>=14.15.0'} - requiresBuild: true - dependencies: - color: 4.2.3 - detect-libc: 2.0.2 - node-addon-api: 5.1.0 - prebuild-install: 7.1.1 - semver: 7.5.4 - simple-get: 4.0.1 - tar-fs: 2.1.1 - tunnel-agent: 0.6.0 - dev: false - /sharp@0.32.6: resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} engines: {node: '>=14.15.0'} @@ -18288,7 +18530,6 @@ packages: /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - dev: false /streamx@2.15.0: resolution: {integrity: sha512-HcxY6ncGjjklGs1xsP1aR71INYcsXFJet5CU1CHqihQ2J5nOsbd4OjgjHO42w/4QNv9gZb3BueV+Vxok5pLEXg==} @@ -18578,6 +18819,13 @@ packages: mkdirp: 1.0.4 yallist: 4.0.0 + /taskkill@5.0.0: + resolution: {integrity: sha512-+HRtZ40Vc+6YfCDWCeAsixwxJgMbPY4HHuTgzPYH3JXvqHWUlsCfy+ylXlAKhFNcuLp4xVeWeFBUhDk+7KYUvQ==} + engines: {node: '>=14.16'} + dependencies: + execa: 6.1.0 + dev: true + /telejson@7.2.0: resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==} dependencies: @@ -19174,7 +19422,6 @@ packages: engines: {node: '>=8'} dependencies: '@lukeed/csprng': 1.0.1 - dev: false /ulid@2.3.0: resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} @@ -19417,7 +19664,7 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - /v-code-diff@1.7.2(vue@3.3.12): + /v-code-diff@1.7.2(vue@3.4.3): resolution: {integrity: sha512-y+q8ZHf8GfphYLhcZbjAKcId/h6vZujS71Ryq5u+dI6Jg4ZLTdLrBNVSzYpHywHSSFFfBMdilm6XvVryEaH4+A==} requiresBuild: true peerDependencies: @@ -19430,8 +19677,8 @@ packages: diff: 5.1.0 diff-match-patch: 1.0.5 highlight.js: 11.8.0 - vue: 3.3.12(typescript@5.3.3) - vue-demi: 0.13.11(vue@3.3.12) + vue: 3.4.3(typescript@5.3.3) + vue-demi: 0.13.11(vue@3.4.3) dev: false /v8-to-istanbul@9.1.0: @@ -19635,7 +19882,7 @@ packages: resolution: {integrity: sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==} dev: true - /vue-demi@0.13.11(vue@3.3.12): + /vue-demi@0.13.11(vue@3.4.3): resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} engines: {node: '>=12'} hasBin: true @@ -19647,23 +19894,23 @@ packages: '@vue/composition-api': optional: true dependencies: - vue: 3.3.12(typescript@5.3.3) + vue: 3.4.3(typescript@5.3.3) dev: false - /vue-docgen-api@4.64.1(vue@3.3.12): + /vue-docgen-api@4.64.1(vue@3.4.3): resolution: {integrity: sha512-jbOf7ByE3Zvtuk+429Jorl+eIeh2aB2Fx1GUo3xJd1aByJWE8KDlSEa6b11PB1ze8f0sRUBraRDinICCk0KY7g==} dependencies: '@babel/parser': 7.23.4 '@babel/types': 7.23.4 '@vue/compiler-dom': 3.3.8 - '@vue/compiler-sfc': 3.3.12 + '@vue/compiler-sfc': 3.4.3 ast-types: 0.14.2 hash-sum: 2.0.0 lru-cache: 8.0.4 pug: 3.0.2 recast: 0.22.0 ts-map: 1.0.3 - vue-inbrowser-compiler-independent-utils: 4.64.1(vue@3.3.12) + vue-inbrowser-compiler-independent-utils: 4.64.1(vue@3.4.3) transitivePeerDependencies: - vue dev: true @@ -19686,12 +19933,12 @@ packages: - supports-color dev: true - /vue-inbrowser-compiler-independent-utils@4.64.1(vue@3.3.12): + /vue-inbrowser-compiler-independent-utils@4.64.1(vue@3.4.3): resolution: {integrity: sha512-Hn32n07XZ8j9W8+fmOXPQL+i+W2e/8i6mkH4Ju3H6nR0+cfvmWM95GhczYi5B27+Y8JlCKgAo04IUiYce4mKAw==} peerDependencies: vue: '>=2' dependencies: - vue: 3.3.12(typescript@5.3.3) + vue: 3.4.3(typescript@5.3.3) dev: true /vue-template-compiler@2.7.14: @@ -19701,40 +19948,40 @@ packages: he: 1.2.0 dev: true - /vue-tsc@1.8.25(typescript@5.3.3): - resolution: {integrity: sha512-lHsRhDc/Y7LINvYhZ3pv4elflFADoEOo67vfClAfF2heVHpHmVquLSjojgCSIwzA4F0Pc4vowT/psXCYcfk+iQ==} + /vue-tsc@1.8.27(typescript@5.3.3): + resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} hasBin: true peerDependencies: typescript: '*' dependencies: '@volar/typescript': 1.11.1 - '@vue/language-core': 1.8.25(typescript@5.3.3) + '@vue/language-core': 1.8.27(typescript@5.3.3) semver: 7.5.4 typescript: 5.3.3 dev: true - /vue@3.3.12(typescript@5.3.3): - resolution: {integrity: sha512-jYNv2QmET2OTHsFzfWHMnqgCfqL4zfo97QwofdET+GBRCHhSCHuMTTvNIgeSn0/xF3JRT5OGah6MDwUFN7MPlg==} + /vue@3.4.3(typescript@5.3.3): + resolution: {integrity: sha512-GjN+culMAGv/mUbkIv8zMKItno8npcj5gWlXkSxf1SPTQf8eJ4A+YfHIvQFyL1IfuJcMl3soA7SmN1fRxbf/wA==} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@vue/compiler-dom': 3.3.12 - '@vue/compiler-sfc': 3.3.12 - '@vue/runtime-dom': 3.3.12 - '@vue/server-renderer': 3.3.12(vue@3.3.12) - '@vue/shared': 3.3.12 + '@vue/compiler-dom': 3.4.3 + '@vue/compiler-sfc': 3.4.3 + '@vue/runtime-dom': 3.4.3 + '@vue/server-renderer': 3.4.3(vue@3.4.3) + '@vue/shared': 3.4.3 typescript: 5.3.3 - /vuedraggable@4.1.0(vue@3.3.12): + /vuedraggable@4.1.0(vue@3.4.3): resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} peerDependencies: vue: ^3.0.1 dependencies: sortablejs: 1.14.0 - vue: 3.3.12(typescript@5.3.3) + vue: 3.4.3(typescript@5.3.3) dev: false /w3c-xmlserializer@5.0.0: @@ -20205,22 +20452,6 @@ packages: engines: {vscode: ^1.83.0} dev: false - github.com/misskey-dev/browser-image-resizer/0227e860621e55cbed0aabe6dc601096a7748c4a: - resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/0227e860621e55cbed0aabe6dc601096a7748c4a} - name: browser-image-resizer - version: 2.2.1-misskey.3 - dev: false - - github.com/misskey-dev/sharp-read-bmp/02d9dc189fa7df0c4bea09330be26741772dac01: - resolution: {tarball: https://codeload.github.com/misskey-dev/sharp-read-bmp/tar.gz/02d9dc189fa7df0c4bea09330be26741772dac01} - name: sharp-read-bmp - version: 1.0.0 - dependencies: - decode-bmp: 0.2.1 - decode-ico: 0.4.1 - sharp: 0.31.3 - dev: false - github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.6.5)(@storybook/components@7.6.5)(@storybook/core-events@7.6.5)(@storybook/manager-api@7.6.5)(@storybook/preview-api@7.6.5)(@storybook/theming@7.6.5)(@storybook/types@7.6.5)(react-dom@18.2.0)(react@18.2.0): resolution: {tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640} id: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640 @@ -20252,17 +20483,3 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - - github.com/misskey-dev/summaly/d2a3e07205c3c9769bc5a7b42031c8884b5a25c8: - resolution: {tarball: https://codeload.github.com/misskey-dev/summaly/tar.gz/d2a3e07205c3c9769bc5a7b42031c8884b5a25c8} - name: summaly - version: 4.0.2 - dependencies: - cheerio: 1.0.0-rc.12 - escape-regexp: 0.0.1 - got: 12.6.1 - html-entities: 2.3.2 - iconv-lite: 0.6.3 - jschardet: 3.0.0 - private-ip: 2.3.3 - trace-redirect: 1.0.6 |