diff options
| author | Marie <Marie@kaifa.ch> | 2023-12-23 02:09:23 +0100 |
|---|---|---|
| committer | Marie <Marie@kaifa.ch> | 2023-12-23 02:09:23 +0100 |
| commit | 5db583a3eb61d50de14d875ebf7ecef20490e313 (patch) | |
| tree | 783dd43d2ac660c32e745a4485d499e9ddc43324 /packages | |
| parent | add: Custom MOTDs (diff) | |
| parent | Update CHANGELOG.md (diff) | |
| download | sharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.tar.gz sharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.tar.bz2 sharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.zip | |
merge: upstream
Diffstat (limited to 'packages')
648 files changed, 45691 insertions, 10176 deletions
diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc index d9f047b6ac..0504a2d389 100644 --- a/packages/backend/.swcrc +++ b/packages/backend/.swcrc @@ -11,7 +11,7 @@ "decoratorMetadata": true }, "experimental": { - "keepImportAttributes": true + "keepImportAssertions": true }, "baseUrl": "src", "paths": { diff --git a/packages/backend/generate_api_json.js b/packages/backend/generate_api_json.js new file mode 100644 index 0000000000..5819c60a5f --- /dev/null +++ b/packages/backend/generate_api_json.js @@ -0,0 +1,8 @@ +import { loadConfig } from './built/config.js' +import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js' +import { writeFileSync } from "node:fs"; + +const config = loadConfig(); +const spec = genOpenapiSpec(config); + +writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
\ No newline at end of file diff --git a/packages/backend/migration/1700303245007-supportVerifyMailApi.js b/packages/backend/migration/1700303245007-supportVerifyMailApi.js new file mode 100644 index 0000000000..3ac59ec37a --- /dev/null +++ b/packages/backend/migration/1700303245007-supportVerifyMailApi.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SupportVerifyMailApi1700303245007 { + name = 'SupportVerifyMailApi1700303245007' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "verifymailAuthKey" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "enableVerifymailApi" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableVerifymailApi"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "verifymailAuthKey"`); + } +} diff --git a/packages/backend/migration/1700383825690-hard-mute.js b/packages/backend/migration/1700383825690-hard-mute.js new file mode 100644 index 0000000000..afd3247f5c --- /dev/null +++ b/packages/backend/migration/1700383825690-hard-mute.js @@ -0,0 +1,11 @@ +export class HardMute1700383825690 { + name = 'HardMute1700383825690' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`); + } +} diff --git a/packages/backend/migration/1700902349231-add-bday-index.js b/packages/backend/migration/1700902349231-add-bday-index.js new file mode 100644 index 0000000000..251526fc26 --- /dev/null +++ b/packages/backend/migration/1700902349231-add-bday-index.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddBdayIndex1700902349231 { + name = 'AddBdayIndex1700902349231' + + async up(queryRunner) { + await queryRunner.query(`CREATE INDEX "IDX_de22cd2b445eee31ae51cdbe99" ON "user_profile" (SUBSTR("birthday", 6, 5))`); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_de22cd2b445eee31ae51cdbe99"`); + } +} diff --git a/packages/backend/migration/1702718871541-ffVisibility.js b/packages/backend/migration/1702718871541-ffVisibility.js new file mode 100644 index 0000000000..24b1873134 --- /dev/null +++ b/packages/backend/migration/1702718871541-ffVisibility.js @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ffVisibility1702718871541 { + constructor() { + this.name = 'ffVisibility1702718871541'; + } + async up(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."user_profile_followingvisibility_enum" AS ENUM('public', 'followers', 'private')`); + await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum") WITH INOUT AS ASSIGNMENT`); + await queryRunner.query(`CREATE TYPE "public"."user_profile_followersVisibility_enum" AS ENUM('public', 'followers', 'private')`); + await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum") WITH INOUT AS ASSIGNMENT`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "followingVisibility" "public"."user_profile_followingvisibility_enum" NOT NULL DEFAULT 'public'`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "followersVisibility" "public"."user_profile_followersVisibility_enum" NOT NULL DEFAULT 'public'`); + await queryRunner.query(`UPDATE "user_profile" SET "followingVisibility" = "ffVisibility"`); + await queryRunner.query(`UPDATE "user_profile" SET "followersVisibility" = "ffVisibility"`); + await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum")`); + await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum")`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "ffVisibility"`); + await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`); + } + async down(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private')`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "ffVisibility" "public"."user_profile_ffvisibility_enum" NOT NULL DEFAULT 'public'`); + await queryRunner.query(`CREATE CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum") WITH INOUT AS ASSIGNMENT`); + await queryRunner.query(`UPDATE "user_profile" SET ffVisibility = "user_profile"."followingVisibility"`); + await queryRunner.query(`DROP CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum")`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followersVisibility"`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followingVisibility"`); + await queryRunner.query(`DROP TYPE "public"."user_profile_followersVisibility_enum"`); + await queryRunner.query(`DROP TYPE "public"."user_profile_followingvisibility_enum"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 66d8412855..aaf30bef12 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -4,11 +4,11 @@ "private": true, "type": "module", "engines": { - "node": ">=18.16.0" + "node": ">=20.10.0" }, "scripts": { - "start": "node ./built/index.js", - "start:test": "NODE_ENV=test node ./built/index.js", + "start": "node ./built/boot/entry.js", + "start:test": "NODE_ENV=test node ./built/boot/entry.js", "migrate": "pnpm typeorm migration:run -d ormconfig.js", "revert": "pnpm typeorm migration:revert -d ormconfig.js", "check:connect": "node ./check_connect.js", @@ -16,6 +16,8 @@ "watch:swc": "swc src -d built -D -w", "build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", "watch": "node watch.mjs", + "restart": "pnpm build && pnpm start", + "dev": "nodemon -w src -e ts,js,mjs,cjs,json --exec \"cross-env NODE_ENV=development pnpm run restart\"", "typecheck": "pnpm --filter megalodon build && tsc --noEmit", "eslint": "eslint --quiet \"src/**/*.ts\"", "lint": "pnpm typecheck && pnpm eslint", @@ -23,7 +25,8 @@ "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-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-and-coverage": "pnpm jest-and-coverage" + "test-and-coverage": "pnpm jest-and-coverage", + "generate-api-json": "node ./generate_api_json.js" }, "optionalDependencies": { "@swc/core-android-arm64": "1.3.11", @@ -57,28 +60,29 @@ "dependencies": { "@aws-sdk/client-s3": "3.412.0", "@aws-sdk/lib-storage": "3.412.0", - "@bull-board/api": "5.9.1", - "@bull-board/fastify": "5.9.1", - "@bull-board/ui": "5.9.1", - "@discordapp/twemoji": "14.1.2", - "@fastify/accepts": "4.2.0", + "@bull-board/api": "5.10.2", + "@bull-board/fastify": "5.10.2", + "@bull-board/ui": "5.10.2", + "@discordapp/twemoji": "15.0.2", + "@fastify/accepts": "4.3.0", "@fastify/cookie": "9.2.0", - "@fastify/cors": "8.4.1", + "@fastify/cors": "8.4.2", "@fastify/express": "2.3.0", "@fastify/http-proxy": "9.3.0", "@fastify/multipart": "8.0.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@nestjs/common": "10.2.8", - "@nestjs/core": "10.2.8", - "@nestjs/testing": "10.2.8", + "@nestjs/common": "10.2.10", + "@nestjs/core": "10.2.10", + "@nestjs/testing": "10.2.10", "@peertube/http-signature": "1.7.0", - "@sharkey/sfm-js": "0.23.3", + "@sharkey/sfm-js": "0.24.0", "@simplewebauthn/server": "8.3.5", "@sinonjs/fake-timers": "11.2.2", - "@smithy/node-http-handler": "2.1.5", + "@smithy/node-http-handler": "2.1.10", "@swc/cli": "0.1.63", - "@swc/core": "1.3.96", + "@swc/core": "1.3.100", + "@twemoji/parser": "15.0.0", "accepts": "1.3.8", "ajv": "8.12.0", "archiver": "6.0.1", @@ -87,7 +91,7 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "4.13.3", + "bullmq": "4.15.4", "cacheable-lookup": "7.0.0", "cbor": "9.0.1", "chalk": "5.3.0", @@ -105,7 +109,7 @@ "file-type": "18.7.0", "fluent-ffmpeg": "2.1.2", "form-data": "4.0.0", - "got": "13.0.0", + "got": "14.0.0", "happy-dom": "10.0.3", "hpagent": "1.2.0", "http-link-header": "1.1.1", @@ -114,17 +118,17 @@ "ipaddr.js": "2.1.0", "is-svg": "5.0.0", "js-yaml": "4.1.0", - "jsdom": "22.1.0", + "jsdom": "23.0.1", "json5": "2.2.3", - "jsonld": "8.3.1", - "jsrsasign": "10.8.6", + "jsonld": "8.3.2", + "jsrsasign": "10.9.0", + "meilisearch": "0.36.0", "megalodon": "workspace:*", - "meilisearch": "0.35.0", - "microformats-parser": "1.5.2", + "microformats-parser": "2.0.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", "ms": "3.0.0-canary.1", - "nanoid": "5.0.3", + "nanoid": "5.0.4", "nested-property": "4.0.0", "node-fetch": "3.3.2", "nodemailer": "6.9.7", @@ -132,7 +136,7 @@ "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.2.0", + "otpauth": "9.2.1", "parse5": "7.1.2", "pg": "8.11.3", "pkce-challenge": "4.0.1", @@ -144,9 +148,9 @@ "qrcode": "1.5.3", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.20.8", + "re2": "1.20.9", "redis-lock": "0.1.4", - "reflect-metadata": "0.1.13", + "reflect-metadata": "0.1.14", "rename": "1.0.4", "rss-parser": "3.13.0", "rxjs": "7.8.1", @@ -158,19 +162,18 @@ "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "summaly": "github:misskey-dev/summaly", - "systeminformation": "5.21.17", + "systeminformation": "5.21.20", "tinycolor2": "1.6.0", "tmp": "0.2.1", "tsc-alias": "1.8.8", "tsconfig-paths": "4.2.0", - "twemoji-parser": "14.0.0", "typeorm": "0.3.17", - "typescript": "5.2.2", + "typescript": "5.3.3", "ulid": "2.3.0", "uuid": "^9.0.1", "vary": "1.1.2", "web-push": "3.6.6", - "ws": "8.14.2", + "ws": "8.15.1", "xev": "3.0.2" }, "devDependencies": { @@ -178,7 +181,7 @@ "@simplewebauthn/typescript-types": "8.3.4", "@swc/jest": "0.2.29", "@types/accepts": "1.3.7", - "@types/archiver": "6.0.1", + "@types/archiver": "6.0.2", "@types/bcryptjs": "2.4.6", "@types/body-parser": "1.19.5", "@types/cbor": "6.0.0", @@ -186,28 +189,28 @@ "@types/content-disposition": "0.5.8", "@types/fluent-ffmpeg": "2.1.24", "@types/http-link-header": "1.0.5", - "@types/jest": "29.5.8", + "@types/jest": "29.5.11", "@types/js-yaml": "4.0.9", - "@types/jsdom": "21.1.5", - "@types/jsonld": "1.5.12", + "@types/jsdom": "21.1.6", + "@types/jsonld": "1.5.13", "@types/jsrsasign": "10.5.12", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "20.9.1", + "@types/node": "20.10.5", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.14", "@types/oauth": "0.9.4", "@types/oauth2orize": "1.11.3", "@types/oauth2orize-pkce": "0.1.2", "@types/pg": "8.10.9", - "@types/pug": "2.0.9", - "@types/punycode": "2.1.2", + "@types/pug": "2.0.10", + "@types/punycode": "2.1.3", "@types/qrcode": "1.5.5", "@types/random-seed": "0.3.5", "@types/ratelimiter": "3.4.6", "@types/rename": "1.0.7", - "@types/sanitize-html": "2.9.4", - "@types/semver": "7.5.5", + "@types/sanitize-html": "2.9.5", + "@types/semver": "7.5.6", "@types/sharp": "0.32.0", "@types/simple-oauth2": "5.0.7", "@types/sinonjs__fake-timers": "8.1.5", @@ -216,16 +219,17 @@ "@types/uuid": "^9.0.4", "@types/vary": "1.1.3", "@types/web-push": "3.6.3", - "@types/ws": "8.5.9", - "@typescript-eslint/eslint-plugin": "6.11.0", - "@typescript-eslint/parser": "6.11.0", + "@types/ws": "8.5.10", + "@typescript-eslint/eslint-plugin": "6.14.0", + "@typescript-eslint/parser": "6.14.0", "aws-sdk-client-mock": "3.0.0", "cross-env": "7.0.3", - "eslint": "8.53.0", - "eslint-plugin-import": "2.29.0", + "eslint": "8.56.0", + "eslint-plugin-import": "2.29.1", "execa": "8.0.1", "jest": "29.7.0", "jest-mock": "29.7.0", + "nodemon": "3.0.2", "simple-oauth2": "5.0.0" } } diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 65be275548..2c27a02559 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -16,7 +16,7 @@ import type { AntennasRepository, UserListMembershipsRepository } from '@/models import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() @@ -39,7 +39,7 @@ export class AntennaService implements OnApplicationShutdown { private utilityService: UtilityService, private globalEventService: GlobalEventService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, ) { this.antennasFetched = false; this.antennas = []; @@ -60,11 +60,21 @@ export class AntennaService implements OnApplicationShutdown { lastUsedAt: new Date(body.lastUsedAt), }); break; - case 'antennaUpdated': - this.antennas[this.antennas.findIndex(a => a.id === body.id)] = { - ...body, - lastUsedAt: new Date(body.lastUsedAt), - }; + case 'antennaUpdated': { + const idx = this.antennas.findIndex(a => a.id === body.id); + if (idx >= 0) { + this.antennas[idx] = { + ...body, + lastUsedAt: new Date(body.lastUsedAt), + }; + } else { + // サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり + this.antennas.push({ + ...body, + lastUsedAt: new Date(body.lastUsedAt), + }); + } + } break; case 'antennaDeleted': this.antennas = this.antennas.filter(a => a.id !== body.id); @@ -84,7 +94,7 @@ export class AntennaService implements OnApplicationShutdown { const redisPipeline = this.redisForTimelines.pipeline(); for (const antenna of matchedAntennas) { - this.funoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline); + this.fanoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline); this.globalEventService.publishAntennaStream(antenna.id, 'note', note); } diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index b3ab901fed..fa868ff8b0 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -4,6 +4,7 @@ */ import { Module } from '@nestjs/common'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AnnouncementService } from './AnnouncementService.js'; @@ -62,7 +63,7 @@ import { FileInfoService } from './FileInfoService.js'; import { SearchService } from './SearchService.js'; import { ClipService } from './ClipService.js'; import { FeaturedService } from './FeaturedService.js'; -import { FunoutTimelineService } from './FunoutTimelineService.js'; +import { FanoutTimelineService } from './FanoutTimelineService.js'; import { ChannelFollowingService } from './ChannelFollowingService.js'; import { RegistryApiService } from './RegistryApiService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js'; @@ -194,7 +195,8 @@ const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: Fi const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; -const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService }; +const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService }; +const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService }; const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService }; const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService }; @@ -330,7 +332,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting SearchService, ClipService, FeaturedService, - FunoutTimelineService, + FanoutTimelineService, + FanoutTimelineEndpointService, ChannelFollowingService, RegistryApiService, ChartLoggerService, @@ -459,7 +462,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $SearchService, $ClipService, $FeaturedService, - $FunoutTimelineService, + $FanoutTimelineService, + $FanoutTimelineEndpointService, $ChannelFollowingService, $RegistryApiService, $ChartLoggerService, @@ -589,7 +593,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting SearchService, ClipService, FeaturedService, - FunoutTimelineService, + FanoutTimelineService, + FanoutTimelineEndpointService, ChannelFollowingService, RegistryApiService, FederationChart, @@ -717,7 +722,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $SearchService, $ClipService, $FeaturedService, - $FunoutTimelineService, + $FanoutTimelineService, + $FanoutTimelineEndpointService, $ChannelFollowingService, $RegistryApiService, $FederationChart, diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index c9da3f77c0..f31cec2b3a 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -3,9 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { URLSearchParams } from 'node:url'; import * as nodemailer from 'nodemailer'; import { Inject, Injectable } from '@nestjs/common'; import { validate as validateEmail } from 'deep-email-validator'; +import { SubOutputFormat } from 'deep-email-validator/dist/output/output.js'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; @@ -13,6 +15,7 @@ import type Logger from '@/logger.js'; import type { UserProfilesRepository } from '@/models/_.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; @Injectable() export class EmailService { @@ -27,6 +30,7 @@ export class EmailService { private metaService: MetaService, private loggerService: LoggerService, + private httpRequestService: HttpRequestService, ) { this.logger = this.loggerService.getLogger('email'); } @@ -160,14 +164,25 @@ export class EmailService { email: emailAddress, }); - const validated = meta.enableActiveEmailValidation ? await validateEmail({ - email: emailAddress, - validateRegex: true, - validateMx: true, - validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので - validateDisposable: true, // 捨てアドかどうかチェック - validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので - }) : { valid: true, reason: null }; + const verifymailApi = meta.enableVerifymailApi && meta.verifymailAuthKey != null; + let validated; + + if (meta.enableActiveEmailValidation && meta.verifymailAuthKey) { + if (verifymailApi) { + validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey); + } else { + validated = meta.enableActiveEmailValidation ? await validateEmail({ + email: emailAddress, + validateRegex: true, + validateMx: true, + validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので + validateDisposable: true, // 捨てアドかどうかチェック + validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので + }) : { valid: true, reason: null }; + } + } else { + validated = { valid: true, reason: null }; + } const available = exist === 0 && validated.valid; @@ -182,4 +197,65 @@ export class EmailService { null, }; } + + private async verifyMail(emailAddress: string, verifymailAuthKey: string): Promise<{ + valid: boolean; + reason: 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | null; + }> { + const endpoint = 'https://verifymail.io/api/' + emailAddress + '?key=' + verifymailAuthKey; + const res = await this.httpRequestService.send(endpoint, { + method: 'GET', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json, */*', + }, + }); + + const json = (await res.json()) as { + block: boolean; + catch_all: boolean; + deliverable_email: boolean; + disposable: boolean; + domain: string; + email_address: string; + email_provider: string; + mx: boolean; + mx_fallback: boolean; + mx_host: string[]; + mx_ip: string[]; + mx_priority: { [key: string]: number }; + privacy: boolean; + related_domains: string[]; + }; + + if (json.email_address === undefined) { + return { + valid: false, + reason: 'format', + }; + } + if (json.deliverable_email !== undefined && !json.deliverable_email) { + return { + valid: false, + reason: 'smtp', + }; + } + if (json.disposable) { + return { + valid: false, + reason: 'disposable', + }; + } + if (json.mx !== undefined && !json.mx) { + return { + valid: false, + reason: 'mx', + }; + } + + return { + valid: true, + reason: null, + }; + } } diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts new file mode 100644 index 0000000000..6d857d189e --- /dev/null +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -0,0 +1,192 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiNote } from '@/models/Note.js'; +import { Packed } from '@/misc/json-schema.js'; +import type { NotesRepository } from '@/models/_.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { isPureRenote } from '@/misc/is-pure-renote.js'; +import { CacheService } from '@/core/CacheService.js'; +import { isReply } from '@/misc/is-reply.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; + +type TimelineOptions = { + untilId: string | null, + sinceId: string | null, + limit: number, + allowPartial: boolean, + me?: { id: MiUser['id'] } | undefined | null, + useDbFallback: boolean, + redisTimelines: FanoutTimelineName[], + noteFilter?: (note: MiNote) => boolean, + alwaysIncludeMyNotes?: boolean; + ignoreAuthorFromBlock?: boolean; + ignoreAuthorFromMute?: boolean; + excludeNoFiles?: boolean; + excludeReplies?: boolean; + excludeBots?: boolean; + excludePureRenotes: boolean; + dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>, +}; + +@Injectable() +export class FanoutTimelineEndpointService { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private cacheService: CacheService, + private fanoutTimelineService: FanoutTimelineService, + ) { + } + + @bindThis + async timeline(ps: TimelineOptions): Promise<Packed<'Note'>[]> { + return await this.noteEntityService.packMany(await this.getMiNotes(ps), ps.me); + } + + @bindThis + private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> { + let noteIds: string[]; + let shouldFallbackToDb = false; + + // 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える + if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]); + + const shouldPrepend = ps.sinceId && !ps.untilId; + const idCompare: (a: string, b: string) => number = shouldPrepend ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1; + + const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId); + + // TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい + const redisResultIds = Array.from(new Set(redisResult.flat(1))); + + redisResultIds.sort(idCompare); + noteIds = redisResultIds.slice(0, ps.limit); + + shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0); + + if (!shouldFallbackToDb) { + let filter = ps.noteFilter ?? (_note => true); + + if (ps.alwaysIncludeMyNotes && ps.me) { + const me = ps.me; + const parentFilter = filter; + filter = (note) => note.userId === me.id || parentFilter(note); + } + + if (ps.excludeNoFiles) { + const parentFilter = filter; + filter = (note) => note.fileIds.length !== 0 && parentFilter(note); + } + + if (ps.excludeReplies) { + const parentFilter = filter; + filter = (note) => !isReply(note, ps.me?.id) && parentFilter(note); + } + + if (ps.excludeBots) { + const parentFilter = filter; + filter = (note) => !note.user?.isBot && parentFilter(note); + } + + if (ps.excludePureRenotes) { + const parentFilter = filter; + filter = (note) => !isPureRenote(note) && parentFilter(note); + } + + if (ps.me) { + const me = ps.me; + const [ + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + userMutedInstances, + ] = await Promise.all([ + this.cacheService.userMutingsCache.fetch(ps.me.id), + this.cacheService.renoteMutingsCache.fetch(ps.me.id), + this.cacheService.userBlockedCache.fetch(ps.me.id), + this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)), + ]); + + const parentFilter = filter; + filter = (note) => { + if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false; + if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false; + if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false; + if (isInstanceMuted(note, userMutedInstances)) return false; + + return parentFilter(note); + }; + } + + const redisTimeline: MiNote[] = []; + let readFromRedis = 0; + let lastSuccessfulRate = 1; // rateをキャッシュする? + + while ((redisResultIds.length - readFromRedis) !== 0) { + const remainingToRead = ps.limit - redisTimeline.length; + + // DBからの取り直しを減らす初回と同じ割合以上で成功すると仮定するが、クエリの長さを考えて三倍まで + const countToGet = Math.ceil(remainingToRead * Math.min(1.1 / lastSuccessfulRate, 3)); + noteIds = redisResultIds.slice(readFromRedis, readFromRedis + countToGet); + + readFromRedis += noteIds.length; + + const gotFromDb = await this.getAndFilterFromDb(noteIds, filter, idCompare); + redisTimeline.push(...gotFromDb); + lastSuccessfulRate = gotFromDb.length / noteIds.length; + + if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) { + // 十分Redisからとれた + const result = redisTimeline.slice(0, ps.limit); + if (shouldPrepend) result.reverse(); + return result; + } + } + + // まだ足りない分はDBにフォールバック + const remainingToRead = ps.limit - redisTimeline.length; + let dbUntil: string | null; + let dbSince: string | null; + if (shouldPrepend) { + redisTimeline.reverse(); + dbUntil = ps.untilId; + dbSince = noteIds[noteIds.length - 1]; + } else { + dbUntil = noteIds[noteIds.length - 1]; + dbSince = ps.sinceId; + } + const gotFromDb = await ps.dbFallback(dbUntil, dbSince, remainingToRead); + return shouldPrepend ? [...gotFromDb, ...redisTimeline] : [...redisTimeline, ...gotFromDb]; + } + + return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit); + } + + private async getAndFilterFromDb(noteIds: string[], noteFilter: (note: MiNote) => boolean, idCompare: (a: string, b: string) => number): Promise<MiNote[]> { + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + const notes = (await query.getMany()).filter(noteFilter); + + notes.sort((a, b) => idCompare(a.id, b.id)); + + return notes; + } +} diff --git a/packages/backend/src/core/FunoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts index c633c329e5..9b2678fbcd 100644 --- a/packages/backend/src/core/FunoutTimelineService.ts +++ b/packages/backend/src/core/FanoutTimelineService.ts @@ -9,8 +9,37 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; +export type FanoutTimelineName = + // home timeline + | `homeTimeline:${string}` + | `homeTimelineWithFiles:${string}` // only notes with files are included + // local timeline + | `localTimeline` // replies are not included + | `localTimelineWithFiles` // only non-reply notes with files are included + | `localTimelineWithReplies` // only replies are included + | `localTimelineWithReplyTo:${string}` // Only replies to specific local user are included. Parameter is reply user id. + + // antenna + | `antennaTimeline:${string}` + + // user timeline + | `userTimeline:${string}` // replies are not included + | `userTimelineWithFiles:${string}` // only non-reply notes with files are included + | `userTimelineWithReplies:${string}` // only replies are included + | `userTimelineWithChannel:${string}` // only channel notes are included, replies are included + + // user list timelines + | `userListTimeline:${string}` + | `userListTimelineWithFiles:${string}` // only notes with files are included + + // channel timelines + | `channelTimeline:${string}` // replies are included + + // role timelines + | `roleTimeline:${string}` // any notes are included + @Injectable() -export class FunoutTimelineService { +export class FanoutTimelineService { constructor( @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, @@ -20,7 +49,7 @@ export class FunoutTimelineService { } @bindThis - public push(tl: string, id: string, maxlen: number, pipeline: Redis.ChainableCommander) { + public push(tl: FanoutTimelineName, id: string, maxlen: number, pipeline: Redis.ChainableCommander) { // リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、 // 3分以内に投稿されたものでない場合、Redisにある最古のIDより新しい場合のみ追加する if (this.idService.parse(id).date.getTime() > Date.now() - 1000 * 60 * 3) { @@ -41,7 +70,7 @@ export class FunoutTimelineService { } @bindThis - public get(name: string, untilId?: string | null, sinceId?: string | null) { + public get(name: FanoutTimelineName, untilId?: string | null, sinceId?: string | null) { if (untilId && sinceId) { return this.redisForTimelines.lrange('list:' + name, 0, -1) .then(ids => ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1)); @@ -58,7 +87,7 @@ export class FunoutTimelineService { } @bindThis - public getMulti(name: string[], untilId?: string | null, sinceId?: string | null): Promise<string[][]> { + public getMulti(name: FanoutTimelineName[], untilId?: string | null, sinceId?: string | null): Promise<string[][]> { const pipeline = this.redisForTimelines.pipeline(); for (const n of name) { pipeline.lrange('list:' + n, 0, -1); @@ -79,7 +108,7 @@ export class FunoutTimelineService { } @bindThis - public purge(name: string) { + public purge(name: FanoutTimelineName) { return this.redisForTimelines.del('list:' + name); } } diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts index 9617f83880..595383c82c 100644 --- a/packages/backend/src/core/FeaturedService.ts +++ b/packages/backend/src/core/FeaturedService.ts @@ -5,14 +5,17 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { MiNote, MiUser } from '@/models/_.js'; +import type { MiGalleryPost, MiNote, MiUser } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと +export const GALLERY_POSTS_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと +const featuredEpoc = new Date('2023-01-01T00:00:00Z').getTime(); + @Injectable() export class FeaturedService { constructor( @@ -23,7 +26,7 @@ export class FeaturedService { @bindThis private getCurrentWindow(windowRange: number): number { - const passed = new Date().getTime() - new Date(new Date().getFullYear(), 0, 1).getTime(); + const passed = new Date().getTime() - featuredEpoc; return Math.floor(passed / windowRange); } @@ -75,11 +78,27 @@ export class FeaturedService { } @bindThis + private async removeFromRanking(name: string, windowRange: number, element: string): Promise<void> { + const currentWindow = this.getCurrentWindow(windowRange); + const previousWindow = currentWindow - 1; + + const redisPipeline = this.redisClient.pipeline(); + redisPipeline.zrem(`${name}:${currentWindow}`, element); + redisPipeline.zrem(`${name}:${previousWindow}`, element); + await redisPipeline.exec(); + } + + @bindThis public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> { return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score); } @bindThis + public updateGalleryPostsRanking(galleryPostId: MiGalleryPost['id'], score = 1): Promise<void> { + return this.updateRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, galleryPostId, score); + } + + @bindThis public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> { return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score); } @@ -100,6 +119,11 @@ export class FeaturedService { } @bindThis + public getGalleryPostsRanking(threshold: number): Promise<MiGalleryPost['id'][]> { + return this.getRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, threshold); + } + + @bindThis public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise<MiNote['id'][]> { return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold); } @@ -113,4 +137,9 @@ export class FeaturedService { public getHashtagsRanking(threshold: number): Promise<string[]> { return this.getRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, threshold); } + + @bindThis + public removeHashtagsFromRanking(hashtag: string): Promise<void> { + return this.removeFromRanking('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag); + } } diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts index c98b8ea6fc..43e72d2d7b 100644 --- a/packages/backend/src/core/IdService.ts +++ b/packages/backend/src/core/IdService.ts @@ -7,11 +7,11 @@ import { Inject, Injectable } from '@nestjs/common'; import { ulid } from 'ulid'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { genAid, parseAid } from '@/misc/id/aid.js'; -import { genAidx, parseAidx } from '@/misc/id/aidx.js'; -import { genMeid, parseMeid } from '@/misc/id/meid.js'; -import { genMeidg, parseMeidg } from '@/misc/id/meidg.js'; -import { genObjectId, parseObjectId } from '@/misc/id/object-id.js'; +import { genAid, isSafeAidT, parseAid } from '@/misc/id/aid.js'; +import { genAidx, isSafeAidxT, parseAidx } from '@/misc/id/aidx.js'; +import { genMeid, isSafeMeidT, parseMeid } from '@/misc/id/meid.js'; +import { genMeidg, isSafeMeidgT, parseMeidg } from '@/misc/id/meidg.js'; +import { genObjectId, isSafeObjectIdT, parseObjectId } from '@/misc/id/object-id.js'; import { bindThis } from '@/decorators.js'; import { parseUlid } from '@/misc/id/ulid.js'; @@ -26,6 +26,19 @@ export class IdService { this.method = config.id.toLowerCase(); } + @bindThis + public isSafeT(t: number): boolean { + switch (this.method) { + case 'aid': return isSafeAidT(t); + case 'aidx': return isSafeAidxT(t); + case 'meid': return isSafeMeidT(t); + case 'meidg': return isSafeMeidgT(t); + case 'ulid': return t > 0; + case 'objectid': return isSafeObjectIdT(t); + default: throw new Error('unrecognized id generation method'); + } + } + /** * 時間を元にIDを生成します(省略時は現在日時) * @param time 日時 diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts index 508544dc07..80e8020961 100644 --- a/packages/backend/src/core/MetaService.ts +++ b/packages/backend/src/core/MetaService.ts @@ -11,6 +11,7 @@ import { MiMeta } from '@/models/Meta.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { bindThis } from '@/decorators.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() @@ -25,6 +26,7 @@ export class MetaService implements OnApplicationShutdown { @Inject(DI.db) private db: DataSource, + private featuredService: FeaturedService, private globalEventService: GlobalEventService, ) { //this.onMessage = this.onMessage.bind(this); @@ -95,6 +97,8 @@ export class MetaService implements OnApplicationShutdown { @bindThis public async update(data: Partial<MiMeta>): Promise<MiMeta> { + let before: MiMeta | undefined; + const updated = await this.db.transaction(async transactionalEntityManager => { const metas = await transactionalEntityManager.find(MiMeta, { order: { @@ -102,10 +106,10 @@ export class MetaService implements OnApplicationShutdown { }, }); - const meta = metas[0]; + before = metas[0]; - if (meta) { - await transactionalEntityManager.update(MiMeta, meta.id, data); + if (before) { + await transactionalEntityManager.update(MiMeta, before.id, data); const metas = await transactionalEntityManager.find(MiMeta, { order: { @@ -119,6 +123,21 @@ export class MetaService implements OnApplicationShutdown { } }); + if (data.hiddenTags) { + process.nextTick(() => { + const hiddenTags = new Set<string>(data.hiddenTags); + if (before) { + for (const previousHiddenTag of before.hiddenTags) { + hiddenTags.delete(previousHiddenTag); + } + } + + for (const hiddenTag of hiddenTags) { + this.featuredService.removeHashtagsFromRanking(hiddenTag); + } + }); + } + this.globalEventService.publishInternalEvent('metaUpdated', updated); return updated; diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 12f4353ec4..60bf8b3c2a 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -250,6 +250,12 @@ export class MfmService { } } + function fnDefault(node: mfm.MfmFn) { + const el = doc.createElement('i'); + appendChildren(node.children, el); + return el; + } + const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = { bold: (node) => { const el = doc.createElement('b'); @@ -276,17 +282,68 @@ export class MfmService { }, fn: (node) => { - if (node.props.name === 'unixtime') { - const text = node.children[0]!.type === 'text' ? node.children[0].props.text : ''; - const date = new Date(parseInt(text, 10) * 1000); - const el = doc.createElement('time'); - el.setAttribute('datetime', date.toISOString()); - el.textContent = date.toISOString(); - return el; - } else { - const el = doc.createElement('i'); - appendChildren(node.children, el); - return el; + switch (node.props.name) { + case 'unixtime': { + const text = node.children[0].type === 'text' ? node.children[0].props.text : ''; + try { + const date = new Date(parseInt(text, 10) * 1000); + const el = doc.createElement('time'); + el.setAttribute('datetime', date.toISOString()); + el.textContent = date.toISOString(); + return el; + } catch (err) { + return fnDefault(node); + } + } + + case 'ruby': { + if (node.children.length === 1) { + const child = node.children[0]; + const text = child.type === 'text' ? child.props.text : ''; + const rubyEl = doc.createElement('ruby'); + const rtEl = doc.createElement('rt'); + + // ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする + const rpStartEl = doc.createElement('rp'); + rpStartEl.appendChild(doc.createTextNode('(')); + const rpEndEl = doc.createElement('rp'); + rpEndEl.appendChild(doc.createTextNode(')')); + + rubyEl.appendChild(doc.createTextNode(text.split(' ')[0])); + rtEl.appendChild(doc.createTextNode(text.split(' ')[1])); + rubyEl.appendChild(rpStartEl); + rubyEl.appendChild(rtEl); + rubyEl.appendChild(rpEndEl); + return rubyEl; + } else { + const rt = node.children.at(-1); + + if (!rt) { + return fnDefault(node); + } + + const text = rt.type === 'text' ? rt.props.text : ''; + const rubyEl = doc.createElement('ruby'); + const rtEl = doc.createElement('rt'); + + // ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする + const rpStartEl = doc.createElement('rp'); + rpStartEl.appendChild(doc.createTextNode('(')); + const rpEndEl = doc.createElement('rp'); + rpEndEl.appendChild(doc.createTextNode(')')); + + appendChildren(node.children.slice(0, node.children.length - 1), rubyEl); + rtEl.appendChild(doc.createTextNode(text.trim())); + rubyEl.appendChild(rpStartEl); + rubyEl.appendChild(rtEl); + rubyEl.appendChild(rpEndEl); + return rubyEl; + } + } + + default: { + return fnDefault(node); + } } }, diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 939662707f..270730c723 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -54,9 +54,10 @@ import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +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'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -194,7 +195,7 @@ export class NoteCreateService implements OnApplicationShutdown { private idService: IdService, private globalEventService: GlobalEventService, private queueService: QueueService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private noteReadService: NoteReadService, private notificationService: NotificationService, private relayService: RelayService, @@ -461,7 +462,7 @@ export class NoteCreateService implements OnApplicationShutdown { } // Check blocking - if (data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)) { + if (this.isQuote(data)) { if (data.renote.userHost === null) { if (data.renote.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id); @@ -804,7 +805,7 @@ export class NoteCreateService implements OnApplicationShutdown { // If it is renote if (data.renote) { - const type = data.text ? 'quote' : 'renote'; + const type = this.isQuote(data) ? 'quote' : 'renote'; // Notify if (data.renote.userHost === null) { @@ -1011,6 +1012,12 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis + private isQuote(note: Option): note is Option & { renote: MiNote } { + // sync with misc/is-quote.ts + return !!note.renote && (!!note.text || !!note.cw || (!!note.files && !!note.files.length) || !!note.poll); + } + + @bindThis private incRenoteCount(renote: MiNote) { this.notesRepository.createQueryBuilder().update() .set({ @@ -1075,7 +1082,7 @@ export class NoteCreateService implements OnApplicationShutdown { private async renderNoteOrRenoteActivity(data: Option, note: MiNote) { if (data.localOnly) return null; - const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0) + const content = data.renote && !this.isQuote(data) ? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note) : this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note); @@ -1125,9 +1132,9 @@ export class NoteCreateService implements OnApplicationShutdown { const r = this.redisForTimelines.pipeline(); if (note.channelId) { - this.funoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); + this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); - this.funoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); const channelFollowings = await this.channelFollowingsRepository.find({ where: { @@ -1137,9 +1144,9 @@ export class NoteCreateService implements OnApplicationShutdown { }); for (const channelFollowing of channelFollowings) { - this.funoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } } else { @@ -1173,13 +1180,13 @@ export class NoteCreateService implements OnApplicationShutdown { if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue; // 「自分自身への返信 or そのフォロワーへの返信」のどちらでもない場合 - if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === following.followerId)) { + if (isReply(note, following.followerId)) { if (!following.withReplies) continue; } - this.funoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } @@ -1191,40 +1198,43 @@ export class NoteCreateService implements OnApplicationShutdown { ) continue; // 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合 - if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === userListMembership.userListUserId)) { + if (isReply(note, userListMembership.userListUserId)) { if (!userListMembership.withReplies) continue; } - this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); + this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); } } if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL - this.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } // 自分自身以外への返信 - if (note.replyId && note.replyUserId !== note.userId) { - this.funoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + if (isReply(note)) { + this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { - this.funoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); + this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); + if (note.replyUserHost == null) { + this.fanoutTimelineService.push(`localTimelineWithReplyTo:${note.replyUserId}`, note.id, 300 / 10, r); + } } } else { - this.funoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); } if (note.visibility === 'public' && note.userHost == null) { - this.funoutTimelineService.push('localTimeline', note.id, 1000, r); + this.fanoutTimelineService.push('localTimeline', note.id, 1000, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); + this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); } } } diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index c4314c65ac..dae3f485ff 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -44,7 +44,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -191,7 +191,7 @@ export class NoteEditService implements OnApplicationShutdown { private idService: IdService, private globalEventService: GlobalEventService, private queueService: QueueService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private noteReadService: NoteReadService, private notificationService: NotificationService, private relayService: RelayService, @@ -786,9 +786,9 @@ export class NoteEditService implements OnApplicationShutdown { const r = this.redisForTimelines.pipeline(); if (note.channelId) { - this.funoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); + this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); - this.funoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); const channelFollowings = await this.channelFollowingsRepository.find({ where: { @@ -798,9 +798,9 @@ export class NoteEditService implements OnApplicationShutdown { }); for (const channelFollowing of channelFollowings) { - this.funoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } } else { @@ -838,9 +838,9 @@ export class NoteEditService implements OnApplicationShutdown { if (!following.withReplies) continue; } - this.funoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } @@ -856,36 +856,36 @@ export class NoteEditService implements OnApplicationShutdown { if (!userListMembership.withReplies) continue; } - this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); + this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); } } if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL - this.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } // 自分自身以外への返信 if (note.replyId && note.replyUserId !== note.userId) { - this.funoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { - this.funoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); + this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); } } else { - this.funoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); } if (note.visibility === 'public' && note.userHost == null) { - this.funoutTimelineService.push('localTimeline', note.id, 1000, r); + this.fanoutTimelineService.push('localTimeline', note.id, 1000, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); + this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); } } } diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts index 52abb4c2a1..74e53c5c46 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -77,7 +77,7 @@ export class NotePiningService { } as MiUserNotePining); // Deliver to remote followers - if (this.userEntityService.isLocalUser(user)) { + if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) { this.deliverPinnedChange(user.id, note.id, true); } } @@ -105,7 +105,7 @@ export class NotePiningService { }); // Deliver to remote followers - if (this.userEntityService.isLocalUser(user)) { + if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) { this.deliverPinnedChange(user.id, noteId, false); } } diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 79c1ecc76f..dcd9d7399f 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -6,7 +6,14 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { In } from 'typeorm'; -import type { MiRole, MiRoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js'; +import { ModuleRef } from '@nestjs/core'; +import type { + MiRole, + MiRoleAssignment, + RoleAssignmentsRepository, + RolesRepository, + UsersRepository, +} from '@/models/_.js'; import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js'; import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; @@ -16,12 +23,13 @@ import { CacheService } from '@/core/CacheService.js'; import type { RoleCondFormulaValue } from '@/models/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { IdService } from '@/core/IdService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import type { Packed } from '@/misc/json-schema.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; export type RolePolicies = { gtlAvailable: boolean; @@ -49,6 +57,7 @@ export type RolePolicies = { userEachUserListsLimit: number; rateLimitFactor: number; canImportNotes: boolean; + avatarDecorationLimit: number; }; export const DEFAULT_POLICIES: RolePolicies = { @@ -77,20 +86,27 @@ export const DEFAULT_POLICIES: RolePolicies = { userEachUserListsLimit: 50, rateLimitFactor: 1, canImportNotes: true, + avatarDecorationLimit: 1, }; @Injectable() -export class RoleService implements OnApplicationShutdown { +export class RoleService implements OnApplicationShutdown, OnModuleInit { private rolesCache: MemorySingleCache<MiRole[]>; private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>; + private notificationService: NotificationService; public static AlreadyAssignedError = class extends Error {}; public static NotAssignedError = class extends Error {}; constructor( + private moduleRef: ModuleRef, + @Inject(DI.redis) private redisClient: Redis.Redis, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @@ -109,7 +125,7 @@ export class RoleService implements OnApplicationShutdown { private globalEventService: GlobalEventService, private idService: IdService, private moderationLogService: ModerationLogService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, ) { //this.onMessage = this.onMessage.bind(this); @@ -119,6 +135,10 @@ export class RoleService implements OnApplicationShutdown { this.redisForSub.on('message', this.onMessage); } + async onModuleInit() { + this.notificationService = this.moduleRef.get(NotificationService.name); + } + @bindThis private async onMessage(_: string, data: string): Promise<void> { const obj = JSON.parse(data); @@ -329,6 +349,7 @@ export class RoleService implements OnApplicationShutdown { userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)), rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)), canImportNotes: calc('canImportNotes', vs => vs.some(v => v === true)), + avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)), }; } @@ -427,6 +448,12 @@ export class RoleService implements OnApplicationShutdown { this.globalEventService.publishInternalEvent('userRoleAssigned', created); + if (role.isPublic) { + this.notificationService.createNotification(userId, 'roleAssigned', { + roleId: roleId, + }); + } + if (moderator) { const user = await this.usersRepository.findOneByOrFail({ id: userId }); this.moderationLogService.log(moderator, 'assignRole', { @@ -482,10 +509,10 @@ export class RoleService implements OnApplicationShutdown { public async addNoteToRoleTimeline(note: Packed<'Note'>): Promise<void> { const roles = await this.getUserRoles(note.userId); - const redisPipeline = this.redisClient.pipeline(); + const redisPipeline = this.redisForTimelines.pipeline(); for (const role of roles) { - this.funoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline); + this.fanoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline); this.globalEventService.publishRoleTimelineStream(role.id, 'note', note); } diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index b399ace0ae..57c7e4baba 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -12,6 +12,8 @@ import { MiNote } from '@/models/Note.js'; import { MiUser } from '@/models/_.js'; import type { NotesRepository } from '@/models/_.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { CacheService } from '@/core/CacheService.js'; import { QueryService } from '@/core/QueryService.js'; import { IdService } from '@/core/IdService.js'; import type { Index, MeiliSearch } from 'meilisearch'; @@ -74,6 +76,7 @@ export class SearchService { @Inject(DI.notesRepository) private notesRepository: NotesRepository, + private cacheService: CacheService, private queryService: QueryService, private idService: IdService, ) { @@ -230,8 +233,19 @@ export class SearchService { limit: pagination.limit, }); if (res.hits.length === 0) return []; - const notes = await this.notesRepository.findBy({ + const [ + userIdsWhoMeMuting, + userIdsWhoBlockingMe, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]) : [new Set<string>(), new Set<string>()]; + const notes = (await this.notesRepository.findBy({ id: In(res.hits.map(x => x.id)), + })).filter(note => { + if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; + return true; }); return notes.sort((a, b) => a.id > b.id ? -1 : 1); } else { diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index bd7f298021..d600ffb9d9 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -29,7 +29,7 @@ import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import Logger from '../logger.js'; const logger = new Logger('following/create'); @@ -84,7 +84,7 @@ export class UserFollowingService implements OnModuleInit { private webhookService: WebhookService, private apRendererService: ApRendererService, private accountMoveService: AccountMoveService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private perUserFollowingChart: PerUserFollowingChart, private instanceChart: InstanceChart, ) { @@ -304,8 +304,6 @@ export class UserFollowingService implements OnModuleInit { }); } }); - - this.funoutTimelineService.purge(`homeTimeline:${follower.id}`); } // Publish followed event @@ -373,8 +371,6 @@ export class UserFollowingService implements OnModuleInit { }); } }); - - this.funoutTimelineService.purge(`homeTimeline:${follower.id}`); } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index 702c731fc3..b6e4e1e884 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -3,30 +3,34 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; import * as Redis from 'ioredis'; +import { ModuleRef } from '@nestjs/core'; import type { UserListMembershipsRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import type { MiUserList } from '@/models/UserList.js'; import type { MiUserListMembership } from '@/models/UserListMembership.js'; import { IdService } from '@/core/IdService.js'; +import type { GlobalEvents } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { bindThis } from '@/decorators.js'; -import { RoleService } from '@/core/RoleService.js'; import { QueueService } from '@/core/QueueService.js'; import { RedisKVCache } from '@/misc/cache.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import { RoleService } from '@/core/RoleService.js'; @Injectable() -export class UserListService implements OnApplicationShutdown { +export class UserListService implements OnApplicationShutdown, OnModuleInit { public static TooManyUsersError = class extends Error {}; public membersCache: RedisKVCache<Set<string>>; + private roleService: RoleService; constructor( + private moduleRef: ModuleRef, + @Inject(DI.redis) private redisClient: Redis.Redis, @@ -38,7 +42,6 @@ export class UserListService implements OnApplicationShutdown { private userEntityService: UserEntityService, private idService: IdService, - private roleService: RoleService, private globalEventService: GlobalEventService, private proxyAccountService: ProxyAccountService, private queueService: QueueService, @@ -54,6 +57,10 @@ export class UserListService implements OnApplicationShutdown { this.redisForSub.on('message', this.onMessage); } + async onModuleInit() { + this.roleService = this.moduleRef.get(RoleService.name); + } + @bindThis private async onMessage(_: string, data: string): Promise<void> { const obj = JSON.parse(data); diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 5a7b07e666..d8616d293d 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -306,9 +306,15 @@ export class ApInboxService { this.logger.info(`Creating the (Re)Note: ${uri}`); const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc); + const createdAt = activity.published ? new Date(activity.published) : null; + + if (createdAt && createdAt < this.idService.parse(renote.id).date) { + this.logger.warn('skip: malformed createdAt'); + return; + } await this.noteCreateService.create(actor, { - createdAt: activity.published ? new Date(activity.published) : null, + createdAt, renote, visibility: activityAudience.visibility, visibleUsers: activityAudience.visibleUsers, diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 72488b1c37..2595783e04 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -97,6 +97,10 @@ export class ApNoteService { return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); } + if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) { + return new Error('invalid Note: published timestamp is malformed'); + } + return null; } diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index f74594ff0c..704081ed00 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -15,8 +15,8 @@ import type { Packed } from '@/misc/json-schema.js'; import { bindThis } from '@/decorators.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { FilterUnionByProperty, notificationTypes } from '@/types.js'; +import { RoleEntityService } from './RoleEntityService.js'; import type { OnModuleInit } from '@nestjs/common'; -import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; @@ -27,7 +27,7 @@ const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 're export class NotificationEntityService implements OnModuleInit { private userEntityService: UserEntityService; private noteEntityService: NoteEntityService; - private customEmojiService: CustomEmojiService; + private roleEntityService: RoleEntityService; constructor( private moduleRef: ModuleRef, @@ -43,14 +43,13 @@ export class NotificationEntityService implements OnModuleInit { //private userEntityService: UserEntityService, //private noteEntityService: NoteEntityService, - //private customEmojiService: CustomEmojiService, ) { } onModuleInit() { this.userEntityService = this.moduleRef.get('UserEntityService'); this.noteEntityService = this.moduleRef.get('NoteEntityService'); - this.customEmojiService = this.moduleRef.get('CustomEmojiService'); + this.roleEntityService = this.moduleRef.get('RoleEntityService'); } @bindThis @@ -81,6 +80,7 @@ export class NotificationEntityService implements OnModuleInit { detail: false, }) ) : undefined; + const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined; return await awaitAll({ id: notification.id, @@ -92,6 +92,9 @@ export class NotificationEntityService implements OnModuleInit { ...(notification.type === 'reaction' ? { reaction: notification.reaction, } : {}), + ...(notification.type === 'roleAssigned' ? { + role: role, + } : {}), ...(notification.type === 'achievementEarned' ? { achievement: notification.achievement, } : {}), @@ -198,12 +201,14 @@ export class NotificationEntityService implements OnModuleInit { }); } else if (notification.type === 'renote:grouped') { const users = await Promise.all(notification.userIds.map(userId => { - const user = hint?.packedUsers != null - ? hint.packedUsers.get(userId) - : this.userEntityService.pack(userId!, { id: meId }, { - detail: false, - }); - return user; + const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(userId) : null; + if (packedUser) { + return packedUser; + } + + return this.userEntityService.pack(userId, { id: meId }, { + detail: false, + }); })); return await awaitAll({ id: notification.id, @@ -214,6 +219,8 @@ export class NotificationEntityService implements OnModuleInit { }); } + const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined; + return await awaitAll({ id: notification.id, createdAt: new Date(notification.createdAt).toISOString(), @@ -224,6 +231,9 @@ export class NotificationEntityService implements OnModuleInit { ...(notification.type === 'reaction' ? { reaction: notification.reaction, } : {}), + ...(notification.type === 'roleAssigned' ? { + role: role, + } : {}), ...(notification.type === 'achievementEarned' ? { achievement: notification.achievement, } : {}), diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index b9d5f27d54..adb7dfbf86 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -361,13 +361,13 @@ export class UserEntityService implements OnModuleInit { const mastoapi = !opts.detail ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null; const followingCount = profile == null ? null : - (profile.ffVisibility === 'public') || isMe ? user.followingCount : - (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : + (profile.followingVisibility === 'public') || isMe ? user.followingCount : + (profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : null; const followersCount = profile == null ? null : - (profile.ffVisibility === 'public') || isMe ? user.followersCount : - (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : + (profile.followersVisibility === 'public') || isMe ? user.followersCount : + (profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : null; const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null; @@ -395,6 +395,8 @@ export class UserEntityService implements OnModuleInit { id: ud.id, angle: ud.angle || undefined, flipH: ud.flipH || undefined, + offsetX: ud.offsetX || undefined, + offsetY: ud.offsetY || undefined, url: decorations.find(d => d.id === ud.id)!.url, }))) : [], isBot: user.isBot, @@ -452,7 +454,8 @@ export class UserEntityService implements OnModuleInit { pinnedPageId: profile!.pinnedPageId, pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null, publicReactions: profile!.publicReactions, - ffVisibility: profile!.ffVisibility, + followersVisibility: profile!.followersVisibility, + followingVisibility: profile!.followingVisibility, twoFactorEnabled: profile!.twoFactorEnabled, usePasswordLessLogin: profile!.usePasswordLessLogin, securityKeys: profile!.twoFactorEnabled @@ -511,6 +514,7 @@ export class UserEntityService implements OnModuleInit { hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), unreadNotificationsCount: notificationsInfo?.unreadCount, mutedWords: profile!.mutedWords, + hardMutedWords: profile!.hardMutedWords, mutedInstances: profile!.mutedInstances, mutingNotificationTypes: [], // 後方互換性のため notificationRecieveConfig: profile!.notificationRecieveConfig, diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts index 2dd079bd7c..04c2f2e913 100644 --- a/packages/backend/src/misc/emoji-regex.ts +++ b/packages/backend/src/misc/emoji-regex.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -// taken from twemoji-parser/dist/lib/regex.js -const twemojiRegex = /(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83e\udef1\ud83c\udffb\u200d\ud83e\udef2\ud83c[\udffc-\udfff]|\ud83e\udef1\ud83c\udffc\u200d\ud83e\udef2\ud83c[\udffb\udffd-\udfff]|\ud83e\udef1\ud83c\udffd\u200d\ud83e\udef2\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\udef1\ud83c\udffe\u200d\ud83e\udef2\ud83c[\udffb-\udffd\udfff]|\ud83e\udef1\ud83c\udfff\u200d\ud83e\udef2\ud83c[\udffb-\udffe]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83e\udd1d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91]|\ud83e\udd1d)|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd\udec3-\udec5\udef0-\udef6]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udedd-\udedf\udeeb\udeec\udef4-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78-\uddb4\uddb7\uddba\uddbc-\uddcc\uddd0\uddde-\uddff\ude70-\ude74\ude78-\ude7c\ude80-\ude86\ude90-\udeac\udeb0-\udeba\udec0-\udec2\uded0-\uded9\udee0-\udee7]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g; +// taken from @twemoji/parser/dist/lib/regex.js +const twemojiRegex = /(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83e\udef1\ud83c\udffb\u200d\ud83e\udef2\ud83c[\udffc-\udfff]|\ud83e\udef1\ud83c\udffc\u200d\ud83e\udef2\ud83c[\udffb\udffd-\udfff]|\ud83e\udef1\ud83c\udffd\u200d\ud83e\udef2\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\udef1\ud83c\udffe\u200d\ud83e\udef2\ud83c[\udffb-\udffd\udfff]|\ud83e\udef1\ud83c\udfff\u200d\ud83e\udef2\ud83c[\udffb-\udffe]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83e\udd1d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91]|\ud83e\udd1d)|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b|\ud83d\udc26\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|\ud83e\udef0|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd\udec3-\udec5\udef1-\udef8]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udedc-\udedf\udeeb\udeec\udef4-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78-\uddb4\uddb7\uddba\uddbc-\uddcc\uddd0\uddde-\uddff\ude70-\ude7c\ude80-\ude88\ude90-\udebd\udebf-\udec2\udece-\udedb\udee0-\udee8]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g; export const emojiRegex = new RegExp(`(${twemojiRegex.source})`); export const emojiRegexAtStartToEnd = new RegExp(`^(${twemojiRegex.source})$`); diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts index e7b59f262b..de03f6793f 100644 --- a/packages/backend/src/misc/id/aid.ts +++ b/packages/backend/src/misc/id/aid.ts @@ -34,3 +34,7 @@ export function parseAid(id: string): { date: Date; } { const time = parseInt(id.slice(0, 8), 36) + TIME2000; return { date: new Date(time) }; } + +export function isSafeAidT(t: number): boolean { + return t > TIME2000; +} diff --git a/packages/backend/src/misc/id/aidx.ts b/packages/backend/src/misc/id/aidx.ts index bed223225a..9f457f6f0a 100644 --- a/packages/backend/src/misc/id/aidx.ts +++ b/packages/backend/src/misc/id/aidx.ts @@ -41,3 +41,7 @@ export function parseAidx(id: string): { date: Date; } { const time = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000; return { date: new Date(time) }; } + +export function isSafeAidxT(t: number): boolean { + return t > TIME2000; +} diff --git a/packages/backend/src/misc/id/meid.ts b/packages/backend/src/misc/id/meid.ts index 366738de05..7646282edb 100644 --- a/packages/backend/src/misc/id/meid.ts +++ b/packages/backend/src/misc/id/meid.ts @@ -38,3 +38,7 @@ export function parseMeid(id: string): { date: Date; } { date: new Date(parseInt(id.slice(0, 12), 16) - 0x800000000000), }; } + +export function isSafeMeidT(t: number): boolean { + return t > 0; +} diff --git a/packages/backend/src/misc/id/meidg.ts b/packages/backend/src/misc/id/meidg.ts index 426a46970b..f2a55443ef 100644 --- a/packages/backend/src/misc/id/meidg.ts +++ b/packages/backend/src/misc/id/meidg.ts @@ -38,3 +38,7 @@ export function parseMeidg(id: string): { date: Date; } { date: new Date(parseInt(id.slice(1, 12), 16)), }; } + +export function isSafeMeidgT(t: number): boolean { + return t > 0; +} diff --git a/packages/backend/src/misc/id/object-id.ts b/packages/backend/src/misc/id/object-id.ts index 49bd9591c0..f5c3619fdb 100644 --- a/packages/backend/src/misc/id/object-id.ts +++ b/packages/backend/src/misc/id/object-id.ts @@ -38,3 +38,7 @@ export function parseObjectId(id: string): { date: Date; } { date: new Date(parseInt(id.slice(0, 8), 16) * 1000), }; } + +export function isSafeObjectIdT(t: number): boolean { + return t > 0; +} diff --git a/packages/backend/src/misc/is-instance-muted.ts b/packages/backend/src/misc/is-instance-muted.ts index b231058a95..35fe11849d 100644 --- a/packages/backend/src/misc/is-instance-muted.ts +++ b/packages/backend/src/misc/is-instance-muted.ts @@ -3,12 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { MiNote } from '@/models/Note.js'; import type { Packed } from './json-schema.js'; -export function isInstanceMuted(note: Packed<'Note'>, mutedInstances: Set<string>): boolean { - if (mutedInstances.has(note.user.host ?? '')) return true; - if (mutedInstances.has(note.reply?.user.host ?? '')) return true; - if (mutedInstances.has(note.renote?.user.host ?? '')) return true; +export function isInstanceMuted(note: Packed<'Note'> | MiNote, mutedInstances: Set<string>): boolean { + if (mutedInstances.has(note.user?.host ?? '')) return true; + if (mutedInstances.has(note.reply?.user?.host ?? '')) return true; + if (mutedInstances.has(note.renote?.user?.host ?? '')) return true; return false; } diff --git a/packages/backend/src/misc/is-quote.ts b/packages/backend/src/misc/is-quote.ts index 059f6a4b5f..db72d1d57a 100644 --- a/packages/backend/src/misc/is-quote.ts +++ b/packages/backend/src/misc/is-quote.ts @@ -7,5 +7,6 @@ import type { MiNote } from '@/models/Note.js'; // eslint-disable-next-line import/no-default-export export default function(note: MiNote): boolean { + // sync with NoteCreateService.isQuote return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0)); } diff --git a/packages/backend/src/misc/is-reply.ts b/packages/backend/src/misc/is-reply.ts new file mode 100644 index 0000000000..964c2aa153 --- /dev/null +++ b/packages/backend/src/misc/is-reply.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MiUser } from '@/models/User.js'; + +export function isReply(note: any, viewerId?: MiUser['id'] | undefined | null): boolean { + return note.replyId && note.replyUserId !== note.userId && note.replyUserId !== viewerId; +} diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 80c1041c62..176978d35f 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -36,6 +36,9 @@ import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js'; import { packedFlashSchema } from '@/models/json-schema/flash.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; +import { packedSigninSchema } from '@/models/json-schema/signin.js'; +import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js'; +import { packedAdSchema } from '@/models/json-schema/ad.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -47,6 +50,7 @@ export const refs = { User: packedUserSchema, UserList: packedUserListSchema, + Ad: packedAdSchema, Announcement: packedAnnouncementSchema, App: packedAppSchema, Note: packedNoteSchema, @@ -71,6 +75,9 @@ export const refs = { EmojiSimple: packedEmojiSimpleSchema, EmojiDetailed: packedEmojiDetailedSchema, Flash: packedFlashSchema, + Signin: packedSigninSchema, + RoleLite: packedRoleLiteSchema, + Role: packedRoleSchema, }; export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>; diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index b70828f3dd..6aa35b0b40 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -457,6 +457,17 @@ export class MiMeta { public enableActiveEmailValidation: boolean; @Column('boolean', { + default: false, + }) + public enableVerifymailApi: boolean; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public verifymailAuthKey: string | null; + + @Column('boolean', { default: true, }) public enableChartsForRemoteUser: boolean; diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 1d5fc124e2..3bc2edaa0d 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -3,11 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { notificationTypes } from '@/types.js'; import { MiUser } from './User.js'; import { MiNote } from './Note.js'; -import { MiFollowRequest } from './FollowRequest.js'; import { MiAccessToken } from './AccessToken.js'; +import { MiRole } from './Role.js'; export type MiNotification = { type: 'note'; @@ -69,6 +68,11 @@ export type MiNotification = { createdAt: string; notifierId: MiUser['id']; } | { + type: 'roleAssigned'; + id: string; + createdAt: string; + roleId: MiRole['id']; +} | { type: 'achievementEarned'; id: string; createdAt: string; diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 1e1c07bfc4..3db8b398fd 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -166,8 +166,10 @@ export class MiUser { }) public avatarDecorations: { id: string; - angle: number; - flipH: boolean; + angle?: number; + flipH?: boolean; + offsetX?: number; + offsetY?: number; }[]; @Index() diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 8520a09f0b..ae46fbc83c 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -4,7 +4,7 @@ */ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/types.js'; +import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js'; import { id } from './util/id.js'; import { MiUser } from './User.js'; import { MiPage } from './Page.js'; @@ -29,6 +29,7 @@ export class MiUserProfile { }) public location: string | null; + @Index() @Column('char', { length: 10, nullable: true, comment: 'The birthday (YYYY-MM-DD) of the User.', @@ -100,10 +101,16 @@ export class MiUserProfile { public publicReactions: boolean; @Column('enum', { - enum: ffVisibility, + enum: followingVisibilities, + default: 'public', + }) + public followingVisibility: typeof followingVisibilities[number]; + + @Column('enum', { + enum: followersVisibilities, default: 'public', }) - public ffVisibility: typeof ffVisibility[number]; + public followersVisibility: typeof followersVisibilities[number]; @Column('varchar', { length: 128, nullable: true, @@ -222,7 +229,12 @@ export class MiUserProfile { @Column('jsonb', { default: [], }) - public mutedWords: string[][]; + public mutedWords: (string[] | string)[]; + + @Column('jsonb', { + default: [], + }) + public hardMutedWords: (string[] | string)[]; @Column('jsonb', { default: [], diff --git a/packages/backend/src/models/json-schema/ad.ts b/packages/backend/src/models/json-schema/ad.ts new file mode 100644 index 0000000000..649ffcd4dc --- /dev/null +++ b/packages/backend/src/models/json-schema/ad.ts @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedAdSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, + nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + expiresAt: { + type: 'string', + optional: false, + nullable: false, + format: 'date-time', + }, + startsAt: { + type: 'string', + optional: false, + nullable: false, + format: 'date-time', + }, + place: { + type: 'string', + optional: false, + nullable: false, + }, + priority: { + type: 'string', + optional: false, + nullable: false, + }, + ratio: { + type: 'number', + optional: false, + nullable: false, + }, + url: { + type: 'string', + optional: false, + nullable: false, + }, + imageUrl: { + type: 'string', + optional: false, + nullable: false, + }, + memo: { + type: 'string', + optional: false, + nullable: false, + }, + dayOfWeek: { + type: 'integer', + optional: false, + nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/announcement.ts b/packages/backend/src/models/json-schema/announcement.ts index c7e24c7f29..78a98872b2 100644 --- a/packages/backend/src/models/json-schema/announcement.ts +++ b/packages/backend/src/models/json-schema/announcement.ts @@ -42,11 +42,15 @@ export const packedAnnouncementSchema = { type: 'string', optional: false, nullable: false, }, - forYou: { + needConfirmationToRead: { type: 'boolean', optional: false, nullable: false, }, - needConfirmationToRead: { + silence: { + type: 'boolean', + optional: false, nullable: false, + }, + forYou: { type: 'boolean', optional: false, nullable: false, }, diff --git a/packages/backend/src/models/json-schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts index 8f9770cdc5..5b0fa0f15d 100644 --- a/packages/backend/src/models/json-schema/channel.ts +++ b/packages/backend/src/models/json-schema/channel.ts @@ -19,7 +19,7 @@ export const packedChannelSchema = { }, lastNotedAt: { type: 'string', - optional: false, nullable: true, + nullable: true, optional: false, format: 'date-time', }, name: { @@ -28,25 +28,50 @@ export const packedChannelSchema = { }, description: { type: 'string', + optional: false, nullable: true, + }, + userId: { + type: 'string', nullable: true, optional: false, + format: 'id', }, bannerUrl: { type: 'string', format: 'url', nullable: true, optional: false, }, + pinnedNoteIds: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'string', + format: 'id', + }, + }, + color: { + type: 'string', + optional: false, nullable: false, + }, isArchived: { type: 'boolean', optional: false, nullable: false, }, - notesCount: { + usersCount: { type: 'number', nullable: false, optional: false, }, - usersCount: { + notesCount: { type: 'number', nullable: false, optional: false, }, + isSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, + allowRenoteToExternal: { + type: 'boolean', + optional: false, nullable: false, + }, isFollowing: { type: 'boolean', optional: true, nullable: false, @@ -55,30 +80,14 @@ export const packedChannelSchema = { type: 'boolean', optional: true, nullable: false, }, - userId: { - type: 'string', - nullable: true, optional: false, - format: 'id', - }, - pinnedNoteIds: { + pinnedNotes: { type: 'array', - nullable: false, optional: false, + optional: true, nullable: false, items: { - type: 'string', - format: 'id', + type: 'object', + optional: false, nullable: false, + ref: 'Note', }, }, - color: { - type: 'string', - optional: false, nullable: false, - }, - isSensitive: { - type: 'boolean', - optional: false, nullable: false, - }, - allowRenoteToExternal: { - type: 'boolean', - optional: false, nullable: false, - }, }, } as const; diff --git a/packages/backend/src/models/json-schema/clip.ts b/packages/backend/src/models/json-schema/clip.ts index 64f7a2ad9c..1ab96c2b3b 100644 --- a/packages/backend/src/models/json-schema/clip.ts +++ b/packages/backend/src/models/json-schema/clip.ts @@ -44,13 +44,13 @@ export const packedClipSchema = { type: 'boolean', optional: false, nullable: false, }, - isFavorited: { - type: 'boolean', - optional: true, nullable: false, - }, favoritedCount: { type: 'number', optional: false, nullable: false, }, + isFavorited: { + type: 'boolean', + optional: true, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/drive-file.ts b/packages/backend/src/models/json-schema/drive-file.ts index 87f1340812..79f242a711 100644 --- a/packages/backend/src/models/json-schema/drive-file.ts +++ b/packages/backend/src/models/json-schema/drive-file.ts @@ -74,7 +74,7 @@ export const packedDriveFileSchema = { }, url: { type: 'string', - optional: false, nullable: true, + optional: false, nullable: false, format: 'url', }, thumbnailUrl: { diff --git a/packages/backend/src/models/json-schema/drive-folder.ts b/packages/backend/src/models/json-schema/drive-folder.ts index 51107d423f..aaad301303 100644 --- a/packages/backend/src/models/json-schema/drive-folder.ts +++ b/packages/backend/src/models/json-schema/drive-folder.ts @@ -21,6 +21,12 @@ export const packedDriveFolderSchema = { type: 'string', optional: false, nullable: false, }, + parentId: { + type: 'string', + optional: false, nullable: true, + format: 'id', + example: 'xxxxxxxxxx', + }, foldersCount: { type: 'number', optional: true, nullable: false, @@ -29,12 +35,6 @@ export const packedDriveFolderSchema = { type: 'number', optional: true, nullable: false, }, - parentId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - example: 'xxxxxxxxxx', - }, parent: { type: 'object', optional: true, nullable: true, diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index ac4b37fb57..94873716bf 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -79,6 +79,10 @@ export const packedFederationInstanceSchema = { type: 'string', optional: false, nullable: true, }, + isSilenced: { + type: 'boolean', + optional: false, nullable: false, + }, iconUrl: { type: 'string', optional: false, nullable: true, @@ -93,11 +97,6 @@ export const packedFederationInstanceSchema = { type: 'string', optional: false, nullable: true, }, - isSilenced: { - type: "boolean", - optional: false, - nullable: false, - }, infoUpdatedAt: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/models/json-schema/flash.ts b/packages/backend/src/models/json-schema/flash.ts index 9453ba1dce..f08fa7a279 100644 --- a/packages/backend/src/models/json-schema/flash.ts +++ b/packages/backend/src/models/json-schema/flash.ts @@ -22,26 +22,26 @@ export const packedFlashSchema = { optional: false, nullable: false, format: 'date-time', }, - title: { + userId: { type: 'string', optional: false, nullable: false, + format: 'id', }, - summary: { - type: 'string', + user: { + type: 'object', + ref: 'UserLite', optional: false, nullable: false, }, - script: { + title: { type: 'string', optional: false, nullable: false, }, - userId: { + summary: { type: 'string', optional: false, nullable: false, - format: 'id', }, - user: { - type: 'object', - ref: 'UserLite', + script: { + type: 'string', optional: false, nullable: false, }, likedCount: { diff --git a/packages/backend/src/models/json-schema/following.ts b/packages/backend/src/models/json-schema/following.ts index 3a24ebb619..e92cff20a1 100644 --- a/packages/backend/src/models/json-schema/following.ts +++ b/packages/backend/src/models/json-schema/following.ts @@ -22,16 +22,16 @@ export const packedFollowingSchema = { optional: false, nullable: false, format: 'id', }, - followee: { - type: 'object', - optional: true, nullable: false, - ref: 'UserDetailed', - }, followerId: { type: 'string', optional: false, nullable: false, format: 'id', }, + followee: { + type: 'object', + optional: true, nullable: false, + ref: 'UserDetailed', + }, follower: { type: 'object', optional: true, nullable: false, diff --git a/packages/backend/src/models/json-schema/gallery-post.ts b/packages/backend/src/models/json-schema/gallery-post.ts index cf260c0bf5..df7038950c 100644 --- a/packages/backend/src/models/json-schema/gallery-post.ts +++ b/packages/backend/src/models/json-schema/gallery-post.ts @@ -22,14 +22,6 @@ export const packedGalleryPostSchema = { optional: false, nullable: false, format: 'date-time', }, - title: { - type: 'string', - optional: false, nullable: false, - }, - description: { - type: 'string', - optional: false, nullable: true, - }, userId: { type: 'string', optional: false, nullable: false, @@ -40,6 +32,14 @@ export const packedGalleryPostSchema = { ref: 'UserLite', optional: false, nullable: false, }, + title: { + type: 'string', + optional: false, nullable: false, + }, + description: { + type: 'string', + optional: false, nullable: true, + }, fileIds: { type: 'array', optional: true, nullable: false, @@ -70,5 +70,13 @@ export const packedGalleryPostSchema = { type: 'boolean', optional: false, nullable: false, }, + likedCount: { + type: 'number', + optional: false, nullable: false, + }, + isLiked: { + type: 'boolean', + optional: true, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 38c0054b55..aa749943f0 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -127,22 +127,26 @@ export const packedNoteSchema = { channel: { type: 'object', optional: true, nullable: true, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - name: { - type: 'string', - optional: false, nullable: true, - }, - isSensitive: { - type: 'boolean', - optional: true, nullable: false, - }, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + color: { + type: 'string', + optional: false, nullable: false, + }, + isSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, + allowRenoteToExternal: { + type: 'boolean', + optional: false, nullable: false, }, }, }, @@ -182,6 +186,10 @@ export const packedNoteSchema = { optional: false, nullable: false, }, }, + clippedCount: { + type: 'number', + optional: true, nullable: false, + }, myReaction: { type: 'object', diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 27db3bb62c..c6d6e84317 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -42,13 +42,9 @@ export const packedNotificationSchema = { type: 'string', optional: true, nullable: true, }, - choice: { - type: 'number', - optional: true, nullable: true, - }, - invitation: { - type: 'object', - optional: true, nullable: true, + achievement: { + type: 'string', + optional: true, nullable: false, }, body: { type: 'string', @@ -81,14 +77,14 @@ export const packedNotificationSchema = { required: ['user', 'reaction'], }, }, - }, - users: { - type: 'array', - optional: true, nullable: true, - items: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, + users: { + type: 'array', + optional: true, nullable: true, + items: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, }, }, } as const; diff --git a/packages/backend/src/models/json-schema/page.ts b/packages/backend/src/models/json-schema/page.ts index 3f20a4b802..9baacd6884 100644 --- a/packages/backend/src/models/json-schema/page.ts +++ b/packages/backend/src/models/json-schema/page.ts @@ -22,6 +22,32 @@ export const packedPageSchema = { optional: false, nullable: false, format: 'date-time', }, + userId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, + content: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + }, + }, + variables: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + }, + }, title: { type: 'string', optional: false, nullable: false, @@ -34,23 +60,47 @@ export const packedPageSchema = { type: 'string', optional: false, nullable: true, }, - content: { - type: 'array', + hideTitleWhenPinned: { + type: 'boolean', optional: false, nullable: false, }, - variables: { - type: 'array', + alignCenter: { + type: 'boolean', optional: false, nullable: false, }, - userId: { + font: { type: 'string', optional: false, nullable: false, - format: 'id', }, - user: { + script: { + type: 'string', + optional: false, nullable: false, + }, + eyeCatchingImageId: { + type: 'string', + optional: false, nullable: true, + }, + eyeCatchingImage: { type: 'object', - ref: 'UserLite', + optional: false, nullable: true, + ref: 'DriveFile', + }, + attachedFiles: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'DriveFile', + }, + }, + likedCount: { + type: 'number', optional: false, nullable: false, }, + isLiked: { + type: 'boolean', + optional: true, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts new file mode 100644 index 0000000000..b0c6804bb8 --- /dev/null +++ b/packages/backend/src/models/json-schema/role.ts @@ -0,0 +1,158 @@ +const rolePolicyValue = { + type: 'object', + properties: { + value: { + oneOf: [ + { + type: 'integer', + optional: false, nullable: false, + }, + { + type: 'boolean', + optional: false, nullable: false, + }, + ], + }, + priority: { + type: 'integer', + optional: false, nullable: false, + }, + useDefault: { + type: 'boolean', + optional: false, nullable: false, + }, + }, +} as const; + +export const packedRoleLiteSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + name: { + type: 'string', + optional: false, nullable: false, + example: 'New Role', + }, + color: { + type: 'string', + optional: false, nullable: true, + example: '#000000', + }, + iconUrl: { + type: 'string', + optional: false, nullable: true, + }, + description: { + type: 'string', + optional: false, nullable: false, + }, + isModerator: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + isAdministrator: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + displayOrder: { + type: 'integer', + optional: false, nullable: false, + example: 0, + }, + }, +} as const; + +export const packedRoleSchema = { + type: 'object', + allOf: [ + { + type: 'object', + ref: 'RoleLite', + }, + { + type: 'object', + properties: { + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + updatedAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + target: { + type: 'string', + optional: false, nullable: false, + enum: ['manual', 'conditional'], + }, + condFormula: { + type: 'object', + optional: false, nullable: false, + }, + isPublic: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + isExplorable: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + asBadge: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + canEditMembersByModerator: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + policies: { + type: 'object', + optional: false, nullable: false, + properties: { + pinLimit: rolePolicyValue, + canInvite: rolePolicyValue, + clipLimit: rolePolicyValue, + canHideAds: rolePolicyValue, + inviteLimit: rolePolicyValue, + antennaLimit: rolePolicyValue, + gtlAvailable: rolePolicyValue, + ltlAvailable: rolePolicyValue, + webhookLimit: rolePolicyValue, + canPublicNote: rolePolicyValue, + userListLimit: rolePolicyValue, + wordMuteLimit: rolePolicyValue, + alwaysMarkNsfw: rolePolicyValue, + canSearchNotes: rolePolicyValue, + driveCapacityMb: rolePolicyValue, + rateLimitFactor: rolePolicyValue, + inviteLimitCycle: rolePolicyValue, + noteEachClipsLimit: rolePolicyValue, + inviteExpirationTime: rolePolicyValue, + canManageCustomEmojis: rolePolicyValue, + userEachUserListsLimit: rolePolicyValue, + canManageAvatarDecorations: rolePolicyValue, + canUseTranslator: rolePolicyValue, + avatarDecorationLimit: rolePolicyValue, + }, + }, + usersCount: { + type: 'integer', + optional: false, nullable: false, + }, + }, + }, + ], +} as const; diff --git a/packages/backend/src/models/json-schema/signin.ts b/packages/backend/src/models/json-schema/signin.ts new file mode 100644 index 0000000000..d27d2490c5 --- /dev/null +++ b/packages/backend/src/models/json-schema/signin.ts @@ -0,0 +1,26 @@ +export const packedSigninSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + ip: { + type: 'string', + optional: false, nullable: false, + }, + headers: { + type: 'object', + optional: false, nullable: false, + }, + success: { + type: 'boolean', + optional: false, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 5b12050541..af67e62afa 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -3,6 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +const notificationRecieveConfig = { + type: 'object', + nullable: false, optional: true, + properties: { + type: { + type: 'string', + nullable: false, optional: false, + enum: ['all', 'following', 'follower', 'mutualFollow', 'list', 'never'], + }, + }, +} as const; + export const packedUserLiteSchema = { type: 'object', properties: { @@ -49,17 +61,25 @@ export const packedUserLiteSchema = { nullable: false, optional: false, format: 'id', }, + angle: { + type: 'number', + nullable: false, optional: true, + }, + flipH: { + type: 'boolean', + nullable: false, optional: true, + }, url: { type: 'string', format: 'url', nullable: false, optional: false, }, - angle: { + offsetX: { type: 'number', nullable: false, optional: true, }, - flipH: { - type: 'boolean', + offsetY: { + type: 'number', nullable: false, optional: true, }, }, @@ -95,12 +115,67 @@ export const packedUserLiteSchema = { type: 'boolean', nullable: false, optional: true, }, + instance: { + type: 'object', + nullable: false, optional: true, + properties: { + name: { + type: 'string', + nullable: true, optional: false, + }, + softwareName: { + type: 'string', + nullable: true, optional: false, + }, + softwareVersion: { + type: 'string', + nullable: true, optional: false, + }, + iconUrl: { + type: 'string', + nullable: true, optional: false, + }, + faviconUrl: { + type: 'string', + nullable: true, optional: false, + }, + themeColor: { + type: 'string', + nullable: true, optional: false, + }, + }, + }, + emojis: { + type: 'object', + nullable: false, optional: false, + }, onlineStatus: { type: 'string', - format: 'url', - nullable: true, optional: false, + nullable: false, optional: false, enum: ['unknown', 'online', 'active', 'offline'], }, + badgeRoles: { + type: 'array', + nullable: false, optional: true, + items: { + type: 'object', + nullable: false, optional: false, + properties: { + name: { + type: 'string', + nullable: false, optional: false, + }, + iconUrl: { + type: 'string', + nullable: true, optional: false, + }, + displayOrder: { + type: 'number', + nullable: false, optional: false, + }, + }, + }, + }, }, } as const; @@ -117,21 +192,18 @@ export const packedUserDetailedNotMeOnlySchema = { format: 'uri', nullable: true, optional: false, }, - movedToUri: { + movedTo: { type: 'string', format: 'uri', - nullable: true, - optional: false, + nullable: true, optional: false, }, alsoKnownAs: { type: 'array', - nullable: true, - optional: false, + nullable: true, optional: false, items: { type: 'string', format: 'id', - nullable: false, - optional: false, + nullable: false, optional: false, }, }, createdAt: { @@ -191,10 +263,10 @@ export const packedUserDetailedNotMeOnlySchema = { example: '2018-03-12', }, ListenBrainz: { - type: "string", + type: 'string', nullable: true, optional: false, - example: "Steve", + example: 'Steve', }, lang: { type: 'string', @@ -272,6 +344,16 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: false, }, + followingVisibility: { + type: 'string', + nullable: false, optional: false, + enum: ['public', 'followers', 'private'], + }, + followersVisibility: { + type: 'string', + nullable: false, optional: false, + enum: ['public', 'followers', 'private'], + }, twoFactorEnabled: { type: 'boolean', nullable: false, optional: false, @@ -287,6 +369,23 @@ export const packedUserDetailedNotMeOnlySchema = { nullable: false, optional: false, default: false, }, + roles: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'object', + nullable: false, optional: false, + ref: 'RoleLite', + }, + }, + memo: { + type: 'string', + nullable: true, optional: false, + }, + moderationNote: { + type: 'string', + nullable: false, optional: true, + }, //#region relations isFollowing: { type: 'boolean', @@ -320,13 +419,10 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: true, }, - memo: { - type: 'string', - nullable: false, optional: true, - }, notify: { type: 'string', nullable: false, optional: true, + enum: ['normal', 'none'], }, withReplies: { type: 'boolean', @@ -354,29 +450,37 @@ export const packedMeDetailedOnlySchema = { nullable: true, optional: false, format: 'id', }, - injectFeaturedNote: { + isModerator: { type: 'boolean', nullable: true, optional: false, }, - receiveAnnouncementEmail: { + isAdmin: { type: 'boolean', nullable: true, optional: false, }, + injectFeaturedNote: { + type: 'boolean', + nullable: false, optional: false, + }, + receiveAnnouncementEmail: { + type: 'boolean', + nullable: false, optional: false, + }, alwaysMarkNsfw: { type: 'boolean', - nullable: true, optional: false, + nullable: false, optional: false, }, autoSensitive: { type: 'boolean', - nullable: true, optional: false, + nullable: false, optional: false, }, carefulBot: { type: 'boolean', - nullable: true, optional: false, + nullable: false, optional: false, }, autoAcceptFollowed: { type: 'boolean', - nullable: true, optional: false, + nullable: false, optional: false, }, noCrawle: { type: 'boolean', @@ -415,10 +519,23 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, + unreadAnnouncements: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'object', + nullable: false, optional: false, + ref: 'Announcement', + }, + }, hasUnreadAntenna: { type: 'boolean', nullable: false, optional: false, }, + hasUnreadChannel: { + type: 'boolean', + nullable: false, optional: false, + }, hasUnreadNotification: { type: 'boolean', nullable: false, optional: false, @@ -443,6 +560,18 @@ export const packedMeDetailedOnlySchema = { }, }, }, + hardMutedWords: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'string', + nullable: false, optional: false, + }, + }, + }, mutedInstances: { type: 'array', nullable: true, optional: false, @@ -454,15 +583,150 @@ export const packedMeDetailedOnlySchema = { notificationRecieveConfig: { type: 'object', nullable: false, optional: false, + properties: { + app: notificationRecieveConfig, + quote: notificationRecieveConfig, + reply: notificationRecieveConfig, + follow: notificationRecieveConfig, + renote: notificationRecieveConfig, + mention: notificationRecieveConfig, + reaction: notificationRecieveConfig, + pollEnded: notificationRecieveConfig, + receiveFollowRequest: notificationRecieveConfig, + }, }, emailNotificationTypes: { type: 'array', - nullable: true, optional: false, + nullable: false, optional: false, items: { type: 'string', nullable: false, optional: false, }, }, + achievements: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'object', + nullable: false, optional: false, + properties: { + name: { + type: 'string', + nullable: false, optional: false, + }, + unlockedAt: { + type: 'number', + nullable: false, optional: false, + }, + }, + }, + }, + loggedInDays: { + type: 'number', + nullable: false, optional: false, + }, + policies: { + type: 'object', + nullable: false, optional: false, + properties: { + gtlAvailable: { + type: 'boolean', + nullable: false, optional: false, + }, + ltlAvailable: { + type: 'boolean', + nullable: false, optional: false, + }, + canPublicNote: { + type: 'boolean', + nullable: false, optional: false, + }, + canInvite: { + type: 'boolean', + nullable: false, optional: false, + }, + inviteLimit: { + type: 'number', + nullable: false, optional: false, + }, + inviteLimitCycle: { + type: 'number', + nullable: false, optional: false, + }, + inviteExpirationTime: { + type: 'number', + nullable: false, optional: false, + }, + canManageCustomEmojis: { + type: 'boolean', + nullable: false, optional: false, + }, + canManageAvatarDecorations: { + type: 'boolean', + nullable: false, optional: false, + }, + canSearchNotes: { + type: 'boolean', + nullable: false, optional: false, + }, + canUseTranslator: { + type: 'boolean', + nullable: false, optional: false, + }, + canHideAds: { + type: 'boolean', + nullable: false, optional: false, + }, + driveCapacityMb: { + type: 'number', + nullable: false, optional: false, + }, + alwaysMarkNsfw: { + type: 'boolean', + nullable: false, optional: false, + }, + pinLimit: { + type: 'number', + nullable: false, optional: false, + }, + antennaLimit: { + type: 'number', + nullable: false, optional: false, + }, + wordMuteLimit: { + type: 'number', + nullable: false, optional: false, + }, + webhookLimit: { + type: 'number', + nullable: false, optional: false, + }, + clipLimit: { + type: 'number', + nullable: false, optional: false, + }, + noteEachClipsLimit: { + type: 'number', + nullable: false, optional: false, + }, + userListLimit: { + type: 'number', + nullable: false, optional: false, + }, + userEachUserListsLimit: { + type: 'number', + nullable: false, optional: false, + }, + rateLimitFactor: { + type: 'number', + nullable: false, optional: false, + }, + avatarDecorationLimit: { + type: 'number', + nullable: false, optional: false, + }, + }, + }, //#region secrets email: { type: 'string', @@ -478,6 +742,23 @@ export const packedMeDetailedOnlySchema = { items: { type: 'object', nullable: false, optional: false, + properties: { + id: { + type: 'string', + nullable: false, optional: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + name: { + type: 'string', + nullable: false, optional: false, + }, + lastUsed: { + type: 'string', + nullable: false, optional: false, + format: 'date-time', + }, + }, }, }, //#endregion @@ -539,5 +820,13 @@ export const packedUserSchema = { type: 'object', ref: 'UserDetailed', }, + { + type: 'object', + ref: 'UserDetailedNotMe', + }, + { + type: 'object', + ref: 'MeDetailed', + }, ], } as const; diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 31814bf9cd..ea3ecd4ded 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -157,8 +157,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.systemQueueWorker .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => systemLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`)); //#endregion @@ -203,8 +203,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.dbQueueWorker .on('active', (job) => dbLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => dbLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`)); //#endregion @@ -227,8 +227,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.deliverQueueWorker .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) - .on('error', (err: Error) => deliverLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) + .on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`)); //#endregion @@ -238,7 +238,7 @@ export class QueueProcessorService implements OnApplicationShutdown { autorun: false, concurrency: this.config.inboxJobConcurrency ?? 16, limiter: { - max: this.config.inboxJobPerSec ?? 16, + max: this.config.inboxJobPerSec ?? 32, duration: 1000, }, settings: { @@ -251,8 +251,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.inboxQueueWorker .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) - .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => inboxLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`)); //#endregion @@ -275,8 +275,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.webhookDeliverQueueWorker .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) - .on('error', (err: Error) => webhookLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) + .on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`)); //#endregion @@ -304,8 +304,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => relationshipLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`)); //#endregion @@ -327,8 +327,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.objectStorageQueueWorker .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => objectStorageLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`)); //#endregion diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 2c9e2cf24f..68e426b5bc 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -138,7 +138,7 @@ export class ActivityPubServerService { return; } - const algo = match[1]; + const algo = match[1].toUpperCase(); const digestValue = match[2]; if (algo !== 'SHA-256') { @@ -195,11 +195,11 @@ export class ActivityPubServerService { //#region Check ff visibility const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.ffVisibility === 'private') { + if (profile.followersVisibility === 'private') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); return; - } else if (profile.ffVisibility === 'followers') { + } else if (profile.followersVisibility === 'followers') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); return; @@ -287,11 +287,11 @@ export class ActivityPubServerService { //#region Check ff visibility const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.ffVisibility === 'private') { + if (profile.followingVisibility === 'private') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); return; - } else if (profile.ffVisibility === 'followers') { + } else if (profile.followingVisibility === 'followers') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); return; @@ -370,8 +370,9 @@ export class ActivityPubServerService { order: { id: 'DESC' }, }); - const pinnedNotes = await Promise.all(pinings.map(pining => - this.notesRepository.findOneByOrFail({ id: pining.noteId }))); + const pinnedNotes = (await Promise.all(pinings.map(pining => + this.notesRepository.findOneByOrFail({ id: pining.noteId })))) + .filter(note => !note.localOnly && ['public', 'home'].includes(note.visibility)); const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note))); @@ -492,8 +493,7 @@ export class ActivityPubServerService { @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { - // addConstraintStrategy の型定義がおかしいため - (fastify.addConstraintStrategy as any)({ + fastify.addConstraintStrategy({ name: 'apOrHtml', storage() { const store = {} as any; diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index e82ef64dc4..0c7fc8cefe 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -61,7 +61,9 @@ export class FileServerService { public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.addHook('onRequest', (request, reply, done) => { reply.header('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\''); - reply.header('Access-Control-Allow-Origin', '*'); + if (process.env.NODE_ENV === 'development') { + reply.header('Access-Control-Allow-Origin', '*'); + } done(); }); diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index e308b5d3e4..37a120a6f6 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -96,6 +96,11 @@ export class NodeinfoServerService { metadata: { nodeName: meta.name, nodeDescription: meta.description, + nodeAdmins: [{ + name: meta.maintainerName, + email: meta.maintainerEmail, + }], + // deprecated maintainer: { name: meta.maintainerName, email: meta.maintainerEmail, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 4d10e0fa4f..b6d4175c0c 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -122,8 +122,8 @@ export class ServerService implements OnApplicationShutdown { return; } - const name = path.split('@')[0].replace('.webp', ''); - const host = path.split('@')[1]?.replace('.webp', ''); + const name = path.split('@')[0].replace(/\.webp$/i, ''); + const host = path.split('@')[1]?.replace(/\.webp$/i, ''); const emoji = await this.emojisRepository.findOneBy({ // `@.` is the spec of ReactionService.decodeReaction diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 2037856797..ed1b2d4377 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -24,6 +24,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js'; +import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -396,6 +398,8 @@ const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-de const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default }; const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default }; const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; +const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default }; +const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default }; const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default }; @@ -772,6 +776,8 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $admin_avatarDecorations_list, $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, + $admin_unsetUserAvatar, + $admin_unsetUserBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, @@ -1142,6 +1148,8 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $admin_avatarDecorations_list, $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, + $admin_unsetUserAvatar, + $admin_unsetUserBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index f0b3961f94..63379c8878 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -139,7 +139,7 @@ export class SignupApiService { code: invitationCode, }); - if (ticket == null) { + if (ticket == null || ticket.usedById != null) { reply.code(400); return; } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index bf299d6ef4..de858d5304 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -24,6 +24,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js'; +import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -394,6 +396,8 @@ const eps = [ ['admin/avatar-decorations/list', ep___admin_avatarDecorations_list], ['admin/avatar-decorations/update', ep___admin_avatarDecorations_update], ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], + ['admin/unset-user-avatar', ep___admin_unsetUserAvatar], + ['admin/unset-user-banner', ep___admin_unsetUserBanner], ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles], ['admin/drive/cleanup', ep___admin_drive_cleanup], ['admin/drive/files', ep___admin_drive_files], diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index be4fc82f0c..484118cd46 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -13,6 +13,8 @@ import { AbuseUserReportEntityService } from '@/core/entities/AbuseUserReportEnt export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index 070e88f6f3..07f24d2995 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -15,6 +15,8 @@ import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index 60e928ccbe..86f4b0709b 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -14,6 +14,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireAdmin: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts index 686341582b..bc292fd53a 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts @@ -13,6 +13,8 @@ import { ApiError } from '@/server/api/error.js'; export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireAdmin: true, @@ -23,6 +25,11 @@ export const meta = { id: 'cb865949-8af5-4062-a88c-ef55e8786d1d', }, }, + res: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts index 17f792639b..087ae4befc 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -13,8 +13,16 @@ import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, + res: { + type: 'object', + optional: false, + nullable: false, + ref: 'Ad', + }, } as const; export const paramDef = { @@ -61,7 +69,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ad: ad, }); - return ad; + return { + id: ad.id, + expiresAt: ad.expiresAt.toISOString(), + startsAt: ad.startsAt.toISOString(), + dayOfWeek: ad.dayOfWeek, + url: ad.url, + imageUrl: ad.imageUrl, + priority: ad.priority, + ratio: ad.ratio, + place: ad.place, + memo: ad.memo, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts index 8097133a4c..ba655a6aa3 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts @@ -13,6 +13,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts index 29eff89523..12528917dc 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -12,8 +12,21 @@ import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireModerator: true, + res: { + type: 'array', + optional: false, + nullable: false, + items: { + type: 'object', + optional: false, + nullable: false, + ref: 'Ad', + }, + }, } as const; export const paramDef = { @@ -22,7 +35,7 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, - publishing: { type: 'boolean', default: false }, + publishing: { type: 'boolean', default: null, nullable: true }, }, required: [], } as const; @@ -37,12 +50,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId); - if (ps.publishing) { + if (ps.publishing === true) { query.andWhere('ad.expiresAt > :now', { now: new Date() }).andWhere('ad.startsAt <= :now', { now: new Date() }); + } else if (ps.publishing === false) { + query.andWhere('ad.expiresAt <= :now', { now: new Date() }).orWhere('ad.startsAt > :now', { now: new Date() }); } const ads = await query.limit(ps.limit).getMany(); - return ads; + return ads.map(ad => ({ + id: ad.id, + expiresAt: ad.expiresAt.toISOString(), + startsAt: ad.startsAt.toISOString(), + dayOfWeek: ad.dayOfWeek, + url: ad.url, + imageUrl: ad.imageUrl, + memo: ad.memo, + place: ad.place, + priority: ad.priority, + ratio: ad.ratio, + })); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts index d065f9ec50..b83c163004 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -13,6 +13,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index 69c31a05eb..fb432336e4 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -10,6 +10,8 @@ import { AnnouncementService } from '@/core/AnnouncementService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts index 80ec281253..e84e63c666 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts @@ -13,6 +13,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 9630299a6e..e98ef0b169 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -14,6 +14,8 @@ import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index 717866aead..e2ec344899 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -13,6 +13,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts index ec143fcb53..158435ed21 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts @@ -10,6 +10,8 @@ import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireRolePolicy: 'canManageAvatarDecorations', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts index 6f1f386871..06083cc180 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts @@ -12,6 +12,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireRolePolicy: 'canManageAvatarDecorations', errors: { diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts index d9c669377d..49a8718bce 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts @@ -15,6 +15,8 @@ import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireRolePolicy: 'canManageAvatarDecorations', diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts index 5ea9a40762..3d8f3d63de 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts @@ -12,6 +12,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireRolePolicy: 'canManageAvatarDecorations', diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts index 9ef09b172e..adc446d14b 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts @@ -12,6 +12,8 @@ import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireAdmin: true, diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts index e47ecd81cf..1fdbbfb12e 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts @@ -12,6 +12,8 @@ import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireAdmin: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts index 8af44029c5..3f23319a5f 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts @@ -10,6 +10,8 @@ import { QueueService } from '@/core/QueueService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts index 75d689966f..fd8fa46a47 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts @@ -13,6 +13,8 @@ import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts index ac8a70e3da..816bbfbc45 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts @@ -13,6 +13,8 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index 4e5320007e..61cb843558 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -14,6 +14,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts index 66ee4cab3b..5333adb624 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 58600c0eb3..7e484c612b 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -14,6 +14,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', @@ -29,6 +31,8 @@ export const meta = { id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', }, }, + + ref: 'EmojiDetailed', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 9348e279f1..a24c72b9b8 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -7,17 +7,18 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { EmojisRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import { DI } from '@/di-symbols.js'; import { DriveService } from '@/core/DriveService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', @@ -62,50 +63,43 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, - private emojiEntityService: EmojiEntityService, - private idService: IdService, - private globalEventService: GlobalEventService, + private customEmojiService: CustomEmojiService, private driveService: DriveService, ) { super(meta, paramDef, async (ps, me) => { const emoji = await this.emojisRepository.findOneBy({ id: ps.emojiId }); - if (emoji == null) { throw new ApiError(meta.errors.noSuchEmoji); } - const isDuplicate = await this.emojisRepository.findOneBy({ name: emoji.name, host: IsNull() } ); - if (isDuplicate) throw new ApiError(meta.errors.duplicateName); - let driveFile: MiDriveFile; try { // Create file driveFile = await this.driveService.uploadFromUrl({ url: emoji.originalUrl, user: null, force: true }); } catch (e) { + // TODO: need to return Drive Error throw new ApiError(); } - const copied = await this.emojisRepository.insert({ - id: this.idService.gen(), - updatedAt: new Date(), + // Duplication Check + const isDuplicate = await this.customEmojiService.checkDuplicate(emoji.name); + if (isDuplicate) throw new ApiError(meta.errors.duplicateName); + + const addedEmoji = await this.customEmojiService.add({ + driveFile, name: emoji.name, + category: emoji.category, + aliases: emoji.aliases, host: null, - aliases: [], - originalUrl: driveFile.url, - publicUrl: driveFile.webpublicUrl ?? driveFile.url, - type: driveFile.webpublicType ?? driveFile.type, license: emoji.license, - }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); - - this.globalEventService.publishBroadcastStream('emojiAdded', { - emoji: await this.emojiEntityService.packDetailed(copied.id), - }); + isSensitive: emoji.isSensitive, + localOnly: emoji.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + }, me); - return { - id: copied.id, - }; + return this.emojiEntityService.packDetailed(addedEmoji); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts index e6c1bf317f..c483794a40 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index 58aa0b9950..e15af7717b 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts index 208616c0ac..b75616f3cc 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts @@ -8,7 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; export const meta = { - secure: true, + kind: 'write:admin', requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index 855ab8cd24..a383e09338 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -15,6 +15,8 @@ import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index ab16d86a3d..210b3639c3 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -15,6 +15,8 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts index a5dd6d5e3a..8e92db1daf 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts index 515053f57b..5a06b5b32f 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index 8e834ad1dd..b3e9c6df13 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts index 2dc9595a7e..c59d13ad16 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts @@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 04226d8953..61d857b7b0 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -13,6 +13,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts index b63f01bec3..b81297413c 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts @@ -12,6 +12,8 @@ import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts index 6dbfe3c4f5..6cc4e3087f 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts @@ -13,6 +13,8 @@ import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts index 36ea390e45..18884dfca6 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -12,6 +12,8 @@ import { QueueService } from '@/core/QueueService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index 4db52b1052..cf4ac70cc9 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -14,6 +14,8 @@ import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts index 4bd9e7de7f..b81d9857d7 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts @@ -12,7 +12,19 @@ export const meta = { requireCredential: true, requireAdmin: true, + kind: 'read:admin', + tags: ['admin'], + res: { + type: 'array', + items: { + type: 'object', + properties: { + tablename: { type: 'string' }, + indexname: { type: 'string' }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts index f953b889a3..c104f653ef 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts @@ -12,6 +12,8 @@ export const meta = { requireCredential: true, requireAdmin: true, + kind: 'read:admin', + tags: ['admin'], res: { diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts index 6afa824703..76c32f2a9f 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts @@ -12,8 +12,29 @@ import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireModerator: true, + res: { + type: 'array', + optional: false, + nullable: false, + items: { + type: 'object', + optional: false, + nullable: false, + properties: { + ip: { type: 'string' }, + createdAt: { + type: 'string', + optional: false, + nullable: false, + format: 'date-time', + }, + }, + }, + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts index 4a22fd4824..96de772edc 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts @@ -16,6 +16,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, @@ -33,13 +35,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, - properties: { - code: { - type: 'string', - optional: false, nullable: false, - example: 'GR6S02ERUA5VR', - }, - }, + ref: 'InviteCode', }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/invite/list.ts b/packages/backend/src/server/api/endpoints/admin/invite/list.ts index f25d3fcb33..3b7dc72e11 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/list.ts @@ -12,6 +12,8 @@ import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, @@ -21,6 +23,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, + ref: 'InviteCode', }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index f10accaeac..17c81bb76d 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -13,6 +13,8 @@ import { DEFAULT_POLICIES } from '@/core/RoleService.js'; export const meta = { tags: ['meta'], + kind: 'read:admin', + requireCredential: true, requireAdmin: true, @@ -282,6 +284,14 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + enableVerifymailApi: { + type: 'boolean', + optional: false, nullable: false, + }, + verifymailAuthKey: { + type: 'string', + optional: false, nullable: true, + }, enableChartsForRemoteUser: { type: 'boolean', optional: false, nullable: false, @@ -338,6 +348,82 @@ export const meta = { type: 'number', optional: false, nullable: false, }, + backgroundImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + deeplAuthKey: { + type: 'string', + optional: false, nullable: true, + }, + deeplIsPro: { + type: 'boolean', + optional: false, nullable: false, + }, + defaultDarkTheme: { + type: 'string', + optional: false, nullable: true, + }, + defaultLightTheme: { + type: 'string', + optional: false, nullable: true, + }, + description: { + type: 'string', + optional: false, nullable: true, + }, + disableRegistration: { + type: 'boolean', + optional: false, nullable: false, + }, + impressumUrl: { + type: 'string', + optional: false, nullable: true, + }, + maintainerEmail: { + type: 'string', + optional: false, nullable: true, + }, + maintainerName: { + type: 'string', + optional: false, nullable: true, + }, + name: { + type: 'string', + optional: false, nullable: true, + }, + objectStorageS3ForcePathStyle: { + type: 'boolean', + optional: false, nullable: false, + }, + privacyPolicyUrl: { + type: 'string', + optional: false, nullable: true, + }, + repositoryUrl: { + type: 'string', + optional: false, nullable: false, + }, + summalyProxy: { + type: 'string', + optional: false, nullable: true, + }, + themeColor: { + type: 'string', + optional: false, nullable: true, + }, + tosUrl: { + type: 'string', + optional: false, nullable: true, + }, + uri: { + type: 'string', + optional: false, nullable: false, + }, + version: { + type: 'string', + optional: false, nullable: false, + }, }, }, } as const; @@ -444,6 +530,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- deeplIsPro: instance.deeplIsPro, enableIpLogging: instance.enableIpLogging, enableActiveEmailValidation: instance.enableActiveEmailValidation, + enableVerifymailApi: instance.enableVerifymailApi, + verifymailAuthKey: instance.verifymailAuthKey, enableChartsForRemoteUser: instance.enableChartsForRemoteUser, enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances, enableServerMachineStats: instance.enableServerMachineStats, diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts index 4061e1b5df..e2befec50f 100644 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts @@ -13,6 +13,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts index c9142e9885..1d565e8f24 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts @@ -11,6 +11,8 @@ import { QueueService } from '@/core/QueueService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts index 1515ae4c74..30005fc666 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts @@ -11,6 +11,8 @@ import type { DeliverQueue } from '@/core/QueueModule.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts index febe0d07c6..aa8b6edee5 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts @@ -11,6 +11,8 @@ import type { InboxQueue } from '@/core/QueueModule.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts index 0cba5b4e25..8f46cd6375 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts @@ -11,6 +11,8 @@ import { QueueService } from '@/core/QueueService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index 901195e9a5..1d92e2bf86 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -10,6 +10,8 @@ import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, Obj export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts index b675db2b89..53b83560cf 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -12,6 +12,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/relays/list.ts b/packages/backend/src/server/api/endpoints/admin/relays/list.ts index 0633c57ed5..35c8e05487 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/list.ts @@ -10,6 +10,8 @@ import { RelayService } from '@/core/RelayService.js'; export const meta = { tags: ['admin'], + kind: 'read:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts index 661b4243c4..fdc53cb708 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts @@ -10,6 +10,8 @@ import { RelayService } from '@/core/RelayService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index a4a57d8e4e..d37a526db7 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -15,6 +15,8 @@ import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index fb5ac7a335..fb26c82a9d 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -15,6 +15,8 @@ import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts index a0f3edd867..bbd4cfabbe 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts @@ -13,6 +13,8 @@ import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin', 'role'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts index 8451b1955f..ac6085d921 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts @@ -11,8 +11,16 @@ import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin', 'role'], + kind: 'write:admin', + requireCredential: true, requireAdmin: true, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'Role', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts index 7b989050eb..f60d6754a5 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts @@ -13,6 +13,8 @@ import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin', 'role'], + kind: 'write:admin', + requireCredential: true, requireAdmin: true, diff --git a/packages/backend/src/server/api/endpoints/admin/roles/list.ts b/packages/backend/src/server/api/endpoints/admin/roles/list.ts index 3ed4b324dc..30917ce984 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/list.ts @@ -12,8 +12,20 @@ import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; export const meta = { tags: ['admin', 'role'], + kind: 'read:admin', + requireCredential: true, requireModerator: true, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Role', + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/show.ts b/packages/backend/src/server/api/endpoints/admin/roles/show.ts index 5f0accab6f..91e32d95be 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/show.ts @@ -13,6 +13,8 @@ import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; export const meta = { tags: ['admin', 'role'], + kind: 'read:admin', + requireCredential: true, requireModerator: true, @@ -23,6 +25,12 @@ export const meta = { id: '07dc7d34-c0d8-49b7-96c6-db3ce64ee0b3', }, }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'Role', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts index 4c27583111..701fea1ed5 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts @@ -13,6 +13,8 @@ import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin', 'role'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts b/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts index b4e7e29e90..066fc73234 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts @@ -11,6 +11,8 @@ import { MetaService } from '@/core/MetaService.js'; export const meta = { tags: ['admin', 'role'], + kind: 'write:admin', + requireCredential: true, requireAdmin: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index 6031e2363e..6cfcd8ca4a 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -14,6 +14,8 @@ import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin', 'role'], + kind: 'write:admin', + requireCredential: true, requireAdmin: true, diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index b7f9aa0495..6a0f7f9987 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -16,6 +16,8 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin', 'role', 'users'], + kind: 'read:admin', + requireCredential: false, requireAdmin: true, @@ -26,6 +28,20 @@ export const meta = { id: '224eff5e-2488-4b18-b3e7-f50d94421648', }, }, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string', format: 'misskey:id' }, + createdAt: { type: 'string', format: 'date-time' }, + user: { ref: 'UserDetailed' }, + expiresAt: { type: 'string', format: 'date-time', nullable: true }, + }, + required: ['id', 'createdAt', 'user'], + }, + } } as const; export const paramDef = { @@ -78,7 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- id: assign.id, createdAt: this.idService.parse(assign.id).date.toISOString(), user: await this.userEntityService.pack(assign.user!, me, { detail: true }), - expiresAt: assign.expiresAt, + expiresAt: assign.expiresAt?.toISOString() ?? null, }))); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/send-email.ts b/packages/backend/src/server/api/endpoints/admin/send-email.ts index b9f2c6a6f1..d22066909e 100644 --- a/packages/backend/src/server/api/endpoints/admin/send-email.ts +++ b/packages/backend/src/server/api/endpoints/admin/send-email.ts @@ -10,6 +10,8 @@ import { EmailService } from '@/core/EmailService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts index 3169373b0e..d3c3bebff6 100644 --- a/packages/backend/src/server/api/endpoints/admin/server-info.ts +++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts @@ -17,6 +17,8 @@ export const meta = { tags: ['admin', 'meta'], + kind: 'read:admin', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts index f87a5a3574..c82532ed67 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts @@ -14,7 +14,9 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireAdmin: true, + + kind: 'read:admin', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index b1cf24b6ac..76032cfb3d 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -17,6 +17,8 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin', + res: { type: 'object', nullable: false, optional: false, diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index c7f717ff15..f29ebc7fd3 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -17,6 +17,8 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin', + res: { type: 'array', nullable: false, optional: false, diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 9464f4b677..35c3f37481 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -19,6 +19,8 @@ import { QueueService } from '@/core/QueueService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts new file mode 100644 index 0000000000..2309493937 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; + +export const meta = { + tags: ['admin'], + + kind: 'write:admin', + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + if (user.avatarId == null) return; + + await this.usersRepository.update(user.id, { + avatar: null, + avatarId: null, + avatarUrl: null, + avatarBlurhash: null, + }); + + this.moderationLogService.log(me, 'unsetUserAvatar', { + userId: user.id, + userUsername: user.username, + userHost: user.host, + fileId: user.avatarId, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts new file mode 100644 index 0000000000..468c634e5b --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; + +export const meta = { + tags: ['admin'], + + kind: 'write:admin', + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + if (user.bannerId == null) return; + + await this.usersRepository.update(user.id, { + banner: null, + bannerId: null, + bannerUrl: null, + bannerBlurhash: null, + }); + + this.moderationLogService.log(me, 'unsetUserBanner', { + userId: user.id, + userUsername: user.username, + userHost: user.host, + fileId: user.bannerId, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts index 5e523bbc31..8cdd317eae 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts @@ -13,6 +13,8 @@ import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; 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 47deeffe0c..3c5bc7e957 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -12,6 +12,8 @@ import { MetaService } from '@/core/MetaService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireAdmin: true, } as const; @@ -116,6 +118,8 @@ export const paramDef = { objectStorageS3ForcePathStyle: { type: 'boolean' }, enableIpLogging: { type: 'boolean' }, enableActiveEmailValidation: { type: 'boolean' }, + enableVerifymailApi: { type: 'boolean' }, + verifymailAuthKey: { type: 'string', nullable: true }, enableChartsForRemoteUser: { type: 'boolean' }, enableChartsForFederatedInstances: { type: 'boolean' }, enableServerMachineStats: { type: 'boolean' }, @@ -455,6 +459,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.enableActiveEmailValidation = ps.enableActiveEmailValidation; } + if (ps.enableVerifymailApi !== undefined) { + set.enableVerifymailApi = ps.enableVerifymailApi; + } + + if (ps.verifymailAuthKey !== undefined) { + if (ps.verifymailAuthKey === '') { + set.verifymailAuthKey = null; + } else { + set.verifymailAuthKey = ps.verifymailAuthKey; + } + } + if (ps.enableChartsForRemoteUser !== undefined) { set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser; } diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts index bfccc2a2a5..dd0b777373 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts @@ -12,6 +12,8 @@ import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], + kind: 'write:admin', + requireCredential: true, requireModerator: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 9b5911800c..0bf2688b4a 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -12,7 +12,8 @@ import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -70,7 +71,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, private noteReadService: NoteReadService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, + private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -85,12 +87,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchAntenna); } - this.antennasRepository.update(antenna.id, { - isActive: true, - lastUsedAt: new Date(), - }); + // falseだった場合はアンテナの配信先が増えたことを通知したい + const needPublishEvent = !antenna.isActive; + + antenna.isActive = true; + antenna.lastUsedAt = new Date(); + this.antennasRepository.update(antenna.id, antenna); + + if (needPublishEvent) { + this.globalEventService.publishInternalEvent('antennaUpdated', antenna); + } - let noteIds = await this.funoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId); + let noteIds = await this.fanoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId); noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { return []; diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index fae4249c8a..006228ceee 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -4,17 +4,17 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository, MiNote, NotesRepository } from '@/models/_.js'; +import type { ChannelsRepository, NotesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { MiLocalUser } from '@/models/User.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -50,6 +50,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default }, required: ['channelId'], } as const; @@ -57,9 +58,6 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -69,14 +67,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private cacheService: CacheService, private activeUsersChart: ActiveUsersChart, + private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const isRangeSpecified = untilId != null && sinceId != null; + + const serverSettings = await this.metaService.fetch(); const channel = await this.channelsRepository.findOneBy({ id: ps.channelId, @@ -88,64 +88,48 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (me) this.activeUsersChart.read(me); - if (isRangeSpecified || sinceId == null) { - const [ - userIdsWhoMeMuting, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - ]) : [new Set<string>()]; - - let noteIds = await this.funoutTimelineService.get(`channelTimeline:${channel.id}`, untilId, sinceId); - noteIds = noteIds.slice(0, ps.limit); - - if (noteIds.length > 0) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - let timeline = await query.getMany(); - - timeline = timeline.filter(note => { - if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - - return true; - }); - - // TODO: フィルタで件数が減った場合の埋め合わせ処理 - - timeline.sort((a, b) => a.id > b.id ? -1 : 1); - - if (timeline.length > 0) { - return await this.noteEntityService.packMany(timeline, me); - } - } + if (!serverSettings.enableFanoutTimeline) { + return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me); } - //#region fallback to database - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.channelId = :channelId', { channelId: channel.id }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); + return await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + useDbFallback: true, + redisTimelines: [`channelTimeline:${channel.id}`], + excludePureRenotes: false, + dbFallback: async (untilId, sinceId, limit) => { + return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me); + }, + }); + }); + } - if (me) { - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - } - //#endregion + private async getFromDb(ps: { + untilId: string | null, + sinceId: string | null, + limit: number, + channelId: string + }, me: MiLocalUser | null) { + //#region fallback to database + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.channelId = :channelId', { channelId: ps.channelId }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); - const timeline = await query.limit(ps.limit).getMany(); + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + } + //#endregion - return await this.noteEntityService.packMany(timeline, me); - //#endregion - }); + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index cecaded20a..66ac8f664f 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -11,6 +11,23 @@ export const meta = { requireCredential: false, tags: ['meta'], + + res: { + type: 'object', + nullable: true, + properties: { + params: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + type: { type: 'string' }, + }, + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index e143dcfe89..617ca65733 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -36,14 +36,33 @@ export const paramDef = { blocked: { type: 'boolean', nullable: true }, notResponding: { type: 'boolean', nullable: true }, suspended: { type: 'boolean', nullable: true }, - silenced: { type: "boolean", nullable: true }, + silenced: { type: 'boolean', nullable: true }, federating: { type: 'boolean', nullable: true }, subscribing: { type: 'boolean', nullable: true }, publishing: { type: 'boolean', nullable: true }, nsfw: { type: 'boolean', nullable: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, offset: { type: 'integer', default: 0 }, - sort: { type: 'string' }, + sort: { + type: 'string', + nullable: true, + enum: [ + '+pubSub', + '-pubSub', + '+notes', + '-notes', + '+users', + '-users', + '+following', + '-following', + '+followers', + '-followers', + '+firstRetrievedAt', + '-firstRetrievedAt', + '+latestRequestReceivedAt', + '-latestRequestReceivedAt', + ], + }, }, required: [], } as const; @@ -112,18 +131,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } - if (typeof ps.silenced === "boolean") { + if (typeof ps.silenced === 'boolean') { const meta = await this.metaService.fetch(true); if (ps.silenced) { if (meta.silencedHosts.length === 0) { return []; } - query.andWhere("instance.host IN (:...silences)", { + query.andWhere('instance.host IN (:...silences)', { silences: meta.silencedHosts, }); } else if (meta.silencedHosts.length > 0) { - query.andWhere("instance.host NOT IN (:...silences)", { + query.andWhere('instance.host NOT IN (:...silences)', { silences: meta.silencedHosts, }); } diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts index 71eec11235..781c15e742 100644 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts @@ -16,12 +16,9 @@ export const meta = { requireCredential: false, res: { - oneOf: [{ - type: 'object', - ref: 'FederationInstance', - }, { - type: 'null', - }], + type: 'object', + optional: false, nullable: true, + ref: 'FederationInstance', }, } as const; diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index e3ffea7b7e..6548142d41 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -18,6 +18,92 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + res: { + type: 'object', + optional: false, + nullable: false, + properties: { + topSubInstances: { + type: 'array', + optional: false, + nullable: false, + items: { + properties: { + id: { type: 'string' }, + firstRetrievedAt: { type: 'string' }, + host: { type: 'string' }, + usersCount: { type: 'number' }, + notesCount: { type: 'number' }, + followingCount: { type: 'number' }, + followersCount: { type: 'number' }, + isNotResponding: { type: 'boolean' }, + isSuspended: { type: 'boolean' }, + isBlocked: { type: 'boolean' }, + softwareName: { type: 'string' }, + softwareVersion: { type: 'string' }, + openRegistrations: { type: 'boolean' }, + name: { type: 'string' }, + description: { type: 'string' }, + maintainerName: { type: 'string' }, + maintainerEmail: { type: 'string' }, + isSilenced: { type: 'boolean' }, + iconUrl: { type: 'string' }, + faviconUrl: { type: 'string' }, + themeColor: { type: 'string' }, + infoUpdatedAt: { + type: 'string', + nullable: true, + }, + latestRequestReceivedAt: { + type: 'string', + nullable: true, + }, + } + }, + }, + otherFollowersCount: { type: 'number' }, + topPubInstances: { + type: 'array', + optional: false, + nullable: false, + items: { + properties: { + id: { type: 'string' }, + firstRetrievedAt: { type: 'string' }, + host: { type: 'string' }, + usersCount: { type: 'number' }, + notesCount: { type: 'number' }, + followingCount: { type: 'number' }, + followersCount: { type: 'number' }, + isNotResponding: { type: 'boolean' }, + isSuspended: { type: 'boolean' }, + isBlocked: { type: 'boolean' }, + softwareName: { type: 'string' }, + softwareVersion: { type: 'string' }, + openRegistrations: { type: 'boolean' }, + name: { type: 'string' }, + description: { type: 'string' }, + maintainerName: { type: 'string' }, + maintainerEmail: { type: 'string' }, + isSilenced: { type: 'boolean' }, + iconUrl: { type: 'string' }, + faviconUrl: { type: 'string' }, + themeColor: { type: 'string' }, + infoUpdatedAt: { + type: 'string', + nullable: true, + }, + latestRequestReceivedAt: { + type: 'string', + nullable: true, + }, + } + }, + }, + otherFollowingCount: { type: 'number' }, + }, + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/fetch-external-resources.ts b/packages/backend/src/server/api/endpoints/fetch-external-resources.ts index d7b46cc666..6391a2f580 100644 --- a/packages/backend/src/server/api/endpoints/fetch-external-resources.ts +++ b/packages/backend/src/server/api/endpoints/fetch-external-resources.ts @@ -32,6 +32,18 @@ export const meta = { id: '693ba8ba-b486-40df-a174-72f8279b56a4', }, }, + + res: { + type: 'object', + properties: { + type: { + type: 'string', + }, + data: { + type: 'string', + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index 37859d8330..b2dee83fe9 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -16,6 +16,18 @@ export const meta = { requireCredential: false, allowGet: true, cacheSec: 60 * 3, + + res: { + type: 'object', + properties: { + items: { + type: 'array', + items: { + type: 'object', + }, + } + } + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts index 4fa65ac9aa..674f323734 100644 --- a/packages/backend/src/server/api/endpoints/flash/create.ts +++ b/packages/backend/src/server/api/endpoints/flash/create.ts @@ -27,6 +27,12 @@ export const meta = { errors: { }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'Flash', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts index cbab3a83a4..cea4234065 100644 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts @@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { GalleryPostsRepository } from '@/models/_.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; export const meta = { tags: ['gallery'], @@ -27,25 +28,49 @@ export const meta = { export const paramDef = { type: 'object', - properties: {}, + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + untilId: { type: 'string', format: 'misskey:id' }, + }, required: [], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + private galleryPostsRankingCache: string[] = []; + private galleryPostsRankingCacheLastFetchedAt = 0; + constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, private galleryPostEntityService: GalleryPostEntityService, + private featuredService: FeaturedService, ) { super(meta, paramDef, async (ps, me) => { + let postIds: string[]; + if (this.galleryPostsRankingCacheLastFetchedAt !== 0 && (Date.now() - this.galleryPostsRankingCacheLastFetchedAt < 1000 * 60 * 30)) { + postIds = this.galleryPostsRankingCache; + } else { + postIds = await this.featuredService.getGalleryPostsRanking(100); + this.galleryPostsRankingCache = postIds; + this.galleryPostsRankingCacheLastFetchedAt = Date.now(); + } + + postIds.sort((a, b) => a > b ? -1 : 1); + if (ps.untilId) { + postIds = postIds.filter(id => id < ps.untilId!); + } + postIds = postIds.slice(0, ps.limit); + + if (postIds.length === 0) { + return []; + } + const query = this.galleryPostsRepository.createQueryBuilder('post') - .andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) }) - .andWhere('post.likedCount > 0') - .orderBy('post.likedCount', 'DESC'); + .where('post.id IN (:...postIds)', { postIds: postIds }); - const posts = await query.limit(10).getMany(); + const posts = await query.getMany(); return await this.galleryPostEntityService.packMany(posts, me); }); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index 561b2492ab..cc424261b4 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.js'; +import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -57,6 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.galleryLikesRepository) private galleryLikesRepository: GalleryLikesRepository, + private featuredService: FeaturedService, private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { @@ -88,6 +90,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- userId: me.id, }); + // ランキング更新 + if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) { + await this.featuredService.updateGalleryPostsRanking(post.id, 1); + } + this.galleryPostsRepository.increment({ id: post.id }, 'likedCount', 1); }); } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index 832b62282f..caa4d45553 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -6,6 +6,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/_.js'; +import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js'; +import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -49,6 +51,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.galleryLikesRepository) private galleryLikesRepository: GalleryLikesRepository, + + private featuredService: FeaturedService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId }); @@ -68,6 +73,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // Delete like await this.galleryLikesRepository.delete(exist.id); + // ランキング更新 + if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) { + await this.featuredService.updateGalleryPostsRanking(post.id, -1); + } + this.galleryPostsRepository.decrement({ id: post.id }, 'likedCount', 1); }); } diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts index 8a61168f25..737d637b7e 100644 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts @@ -16,6 +16,16 @@ export const meta = { requireCredential: false, allowGet: true, cacheSec: 60 * 1, + res: { + type: 'object', + optional: false, nullable: false, + properties: { + count: { + type: 'number', + nullable: false, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index 6033ce5dd4..4161553d28 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -33,6 +33,16 @@ export const meta = { id: '798d6847-b1ed-4f9c-b1f9-163c42655995', }, }, + + res: { + type: 'object', + nullable: false, + optional: false, + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index a6d05507ed..325d54d196 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -37,6 +37,140 @@ export const meta = { id: 'bf32b864-449b-47b8-974e-f9a5468546f1', }, }, + + res: { + type: 'object', + nullable: false, + optional: false, + properties: { + rp: { + type: 'object', + properties: { + id: { + type: 'string', + nullable: true, + }, + }, + }, + user: { + type: 'object', + properties: { + id: { + type: 'string', + }, + name: { + type: 'string', + }, + displayName: { + type: 'string', + }, + }, + }, + challenge: { + type: 'string', + }, + pubKeyCredParams: { + type: 'array', + items: { + type: 'object', + properties: { + type: { + type: 'string', + }, + alg: { + type: 'number', + }, + }, + }, + }, + timeout: { + type: 'number', + nullable: true, + }, + excludeCredentials: { + type: 'array', + nullable: true, + items: { + type: 'object', + properties: { + id: { + type: 'string', + }, + type: { + type: 'string', + }, + transports: { + type: 'array', + items: { + type: 'string', + enum: [ + "ble", + "cable", + "hybrid", + "internal", + "nfc", + "smart-card", + "usb", + ], + }, + }, + }, + }, + }, + authenticatorSelection: { + type: 'object', + nullable: true, + properties: { + authenticatorAttachment: { + type: 'string', + enum: [ + "cross-platform", + "platform", + ], + }, + requireResidentKey: { + type: 'boolean', + }, + userVerification: { + type: 'string', + enum: [ + "discouraged", + "preferred", + "required", + ], + }, + }, + }, + attestation: { + type: 'string', + nullable: true, + enum: [ + "direct", + "enterprise", + "indirect", + "none", + ], + }, + extensions: { + type: 'object', + nullable: true, + properties: { + appid: { + type: 'string', + nullable: true, + }, + credProps: { + type: 'boolean', + nullable: true, + }, + hmacCreateSecret: { + type: 'boolean', + nullable: true, + }, + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index 9b3ae74f86..15e50c49f3 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -27,6 +27,19 @@ export const meta = { id: '78d6c839-20c9-4c66-b90a-fc0542168b48', }, }, + + res: { + type: 'object', + nullable: false, + optional: false, + properties: { + qr: { type: 'string' }, + url: { type: 'string' }, + secret: { type: 'string' }, + label: { type: 'string' }, + issuer: { type: 'string' }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 09f6540a77..ef89f93181 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -13,6 +13,37 @@ export const meta = { requireCredential: true, secure: true, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + name: { + type: 'string', + }, + createdAt: { + type: 'string', + format: 'date-time', + }, + lastUsedAt: { + type: 'string', + format: 'date-time', + }, + permission: { + type: 'array', + uniqueItems: true, + items: { + type: 'string' + }, + } + }, + }, + }, } as const; export const paramDef = { @@ -50,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- id: token.id, name: token.name ?? token.app?.name, createdAt: this.idService.parse(token.id).date.toISOString(), - lastUsedAt: token.lastUsedAt, + lastUsedAt: token.lastUsedAt?.toISOString(), permission: token.permission, }))); }); diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts index 32061c2aa4..a0ed371fb8 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -14,6 +14,36 @@ export const meta = { requireCredential: true, secure: true, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + name: { + type: 'string', + }, + callbackUrl: { + type: 'string', + nullable: true, + }, + permission: { + type: 'array', + uniqueItems: true, + items: { + type: 'string' + }, + }, + isAuthorized: { + type: 'boolean', + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index 86b726e054..f3ba720c2b 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -64,6 +64,10 @@ export const meta = { id: 'b234a14e-9ebe-4581-8000-074b3c215962', }, }, + + res: { + type: 'object', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index 29fa0a29cc..bd6e85a074 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -9,6 +9,10 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, + + res: { + type: 'object', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index 5b460b45d6..2352beb130 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -18,6 +18,10 @@ export const meta = { id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a', }, }, + + res: { + type: 'object', + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index e8c28298ef..4155a43e0d 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -18,6 +18,10 @@ export const meta = { id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a', }, }, + + res: { + type: 'object', + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index 8953ee5d3d..b411cdd3d9 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -9,6 +9,10 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, + + res: { + type: 'object', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts index 1ff994b82c..0aca2a26fe 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts @@ -10,6 +10,28 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, secure: true, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + scopes: { + type: 'array', + items: { + type: 'array', + items: { + type: 'string', + } + } + }, + domain: { + type: 'string', + nullable: true, + }, + }, + }, + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts index 139bede7bc..f82e3f9b28 100644 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts @@ -12,8 +12,17 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - secure: true, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Signin', + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index d3b4649783..090b07be3c 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -41,6 +41,11 @@ export const meta = { id: 'a2defefb-f220-8849-0af6-17f816099323', }, }, + + res: { + type: 'object', + ref: 'UserDetailed', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index aa730716bd..22079de042 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -135,6 +135,11 @@ export const meta = { }, } as const; +const muteWords = { type: 'array', items: { oneOf: [ + { type: 'array', items: { type: 'string' } }, + { type: 'string' }, +] } } as const; + export const paramDef = { type: 'object', properties: { @@ -145,12 +150,14 @@ export const paramDef = { listenbrainz: { ...listenbrainzSchema, nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true }, - avatarDecorations: { type: 'array', maxItems: 1, items: { + avatarDecorations: { type: 'array', maxItems: 16, items: { type: 'object', properties: { id: { type: 'string', format: 'misskey:id' }, angle: { type: 'number', nullable: true, maximum: 0.5, minimum: -0.5 }, flipH: { type: 'boolean', nullable: true }, + offsetX: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 }, + offsetY: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 }, }, required: ['id'], } }, @@ -185,9 +192,11 @@ export const paramDef = { receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, autoSensitive: { type: 'boolean' }, - ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, + followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, + followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, - mutedWords: { type: 'array' }, + mutedWords: muteWords, + hardMutedWords: muteWords, mutedInstances: { type: 'array', items: { type: 'string', } }, @@ -250,17 +259,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.listenbrainz !== undefined) profileUpdates.listenbrainz = ps.listenbrainz; - if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; - if (ps.mutedWords !== undefined) { + if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility; + if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility; + + function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { // TODO: ちゃんと数える - const length = JSON.stringify(ps.mutedWords).length; - if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) { + const length = JSON.stringify(mutedWords).length; + if (length > limit) { throw new ApiError(meta.errors.tooManyMutedWords); } + } + + function validateMuteWordRegex(mutedWords: (string[] | string)[]) { + for (const mutedWord of mutedWords) { + if (typeof mutedWord !== 'string') continue; - // validate regular expression syntax - ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { - const regexp = x.match(/^\/(.+)\/(.*)$/); + const regexp = mutedWord.match(/^\/(.+)\/(.*)$/); if (!regexp) throw new ApiError(meta.errors.invalidRegexp); try { @@ -268,11 +282,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } catch (err) { throw new ApiError(meta.errors.invalidRegexp); } - }); + } + } + + if (ps.mutedWords !== undefined) { + checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + validateMuteWordRegex(ps.mutedWords); profileUpdates.mutedWords = ps.mutedWords; profileUpdates.enableWordMute = ps.mutedWords.length > 0; } + if (ps.hardMutedWords !== undefined) { + checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + validateMuteWordRegex(ps.hardMutedWords); + profileUpdates.hardMutedWords = ps.hardMutedWords; + } if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; @@ -341,16 +365,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.avatarDecorations) { const decorations = await this.avatarDecorationService.getAll(true); - const myRoles = await this.roleService.getUserRoles(user.id); + const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); const allRoles = await this.roleService.getRoles(); const decorationIds = decorations .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) .map(d => d.id); + if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); + updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ id: d.id, angle: d.angle ?? 0, flipH: d.flipH ?? false, + offsetX: d.offsetX ?? 0, + offsetY: d.offsetY ?? 0, })); } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index f00dba4a85..bdc9f9ea8b 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -27,6 +27,33 @@ export const meta = { id: '87a9bb19-111e-4e37-81d3-a3e7426453b0', }, }, + + res: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + name: { type: 'string' }, + on: { + type: 'array', + items: { + type: 'string', + enum: webhookEventTypes, + } + }, + url: { type: 'string' }, + secret: { type: 'string' }, + active: { type: 'boolean' }, + latestSentAt: { type: 'string', format: 'date-time', nullable: true }, + latestStatus: { type: 'integer', nullable: true }, + }, + }, } as const; export const paramDef = { @@ -73,7 +100,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.globalEventService.publishInternalEvent('webhookCreated', webhook); - return webhook; + return { + id: webhook.id, + userId: webhook.userId, + name: webhook.name, + on: webhook.on, + url: webhook.url, + secret: webhook.secret, + active: webhook.active, + latestSentAt: webhook.latestSentAt?.toISOString(), + latestStatus: webhook.latestStatus, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index aa8921fe24..afb2d0509e 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { webhookEventTypes } from '@/models/Webhook.js'; import type { WebhooksRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; @@ -14,6 +15,36 @@ export const meta = { requireCredential: true, kind: 'read:account', + + res: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + name: { type: 'string' }, + on: { + type: 'array', + items: { + type: 'string', + enum: webhookEventTypes, + } + }, + url: { type: 'string' }, + secret: { type: 'string' }, + active: { type: 'boolean' }, + latestSentAt: { type: 'string', format: 'date-time', nullable: true }, + latestStatus: { type: 'integer', nullable: true }, + }, + } + } } as const; export const paramDef = { @@ -33,7 +64,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- userId: me.id, }); - return webhooks; + return webhooks.map(webhook => ( + { + id: webhook.id, + userId: webhook.userId, + name: webhook.name, + on: webhook.on, + url: webhook.url, + secret: webhook.secret, + active: webhook.active, + latestSentAt: webhook.latestSentAt?.toISOString(), + latestStatus: webhook.latestStatus, + } + )); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index f1294bb5c8..5c6dd908b4 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { webhookEventTypes } from '@/models/Webhook.js'; import type { WebhooksRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -23,6 +24,33 @@ export const meta = { id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098', }, }, + + res: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + name: { type: 'string' }, + on: { + type: 'array', + items: { + type: 'string', + enum: webhookEventTypes, + } + }, + url: { type: 'string' }, + secret: { type: 'string' }, + active: { type: 'boolean' }, + latestSentAt: { type: 'string', format: 'date-time', nullable: true }, + latestStatus: { type: 'integer', nullable: true }, + }, + }, } as const; export const paramDef = { @@ -49,7 +77,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchWebhook); } - return webhook; + return { + id: webhook.id, + userId: webhook.userId, + name: webhook.name, + on: webhook.on, + url: webhook.url, + secret: webhook.secret, + active: webhook.active, + latestSentAt: webhook.latestSentAt?.toISOString(), + latestStatus: webhook.latestStatus, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts index 94836283fa..d82fa50e4f 100644 --- a/packages/backend/src/server/api/endpoints/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/invite/create.ts @@ -31,13 +31,7 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - properties: { - code: { - type: 'string', - optional: false, nullable: false, - example: 'GR6S02ERUA5VR', - }, - }, + ref: 'InviteCode', }, } as const; diff --git a/packages/backend/src/server/api/endpoints/invite/list.ts b/packages/backend/src/server/api/endpoints/invite/list.ts index 06139b6806..2107516ce4 100644 --- a/packages/backend/src/server/api/endpoints/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/invite/list.ts @@ -9,7 +9,6 @@ import type { RegistrationTicketsRepository } from '@/models/_.js'; import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; export const meta = { tags: ['meta'], @@ -23,6 +22,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, + ref: 'InviteCode', }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 8c8fdde066..8048ed3ada 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -258,6 +258,33 @@ export const meta = { }, }, }, + backgroundImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + impressumUrl: { + type: 'string', + optional: false, nullable: true, + }, + logoImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + privacyPolicyUrl: { + type: 'string', + optional: false, nullable: true, + }, + serverRules: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, + themeColor: { + type: 'string', + optional: false, nullable: true, + }, }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 14f67cdfcd..27743dfffa 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -262,7 +262,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (renote.channelId && renote.channelId !== ps.channelId) { // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する - const renoteChannel = await this.channelsRepository.findOneById(renote.channelId); + const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId }); if (renoteChannel == null) { // リノートしたいノートが書き込まれているチャンネルが無い throw new ApiError(meta.errors.noSuchChannel); diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index c456874309..31b8d1ad2d 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -9,6 +9,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { FeaturedService } from '@/core/FeaturedService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { CacheService } from '@/core/CacheService.js'; export const meta = { tags: ['notes'], @@ -47,6 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.notesRepository) private notesRepository: NotesRepository, + private cacheService: CacheService, private noteEntityService: NoteEntityService, private featuredService: FeaturedService, ) { @@ -64,16 +67,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } - if (noteIds.length === 0) { - return []; - } - noteIds.sort((a, b) => a > b ? -1 : 1); if (ps.untilId) { noteIds = noteIds.filter(id => id < ps.untilId!); } noteIds = noteIds.slice(0, ps.limit); + if (noteIds.length === 0) { + return []; + } + + const [ + userIdsWhoMeMuting, + userIdsWhoBlockingMe, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]) : [new Set<string>(), new Set<string>()]; + const query = this.notesRepository.createQueryBuilder('note') .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') @@ -83,10 +94,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); - const notes = await query.getMany(); - notes.sort((a, b) => a.id > b.id ? -1 : 1); + const notes = (await query.getMany()).filter(note => { + if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; + + return true; + }); - // TODO: ミュート等考慮 + notes.sort((a, b) => a.id > b.id ? -1 : 1); return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 767c31d434..48b8a3f4b6 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -5,20 +5,20 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, FollowingsRepository, MiNote, ChannelFollowingsRepository } from '@/models/_.js'; +import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -42,6 +42,12 @@ export const meta = { code: 'STL_DISABLED', id: '620763f4-f621-4533-ab33-0577a1a3c342', }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f', + }, }, } as const; @@ -53,6 +59,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, @@ -78,10 +85,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, - private funoutTimelineService: FunoutTimelineService, private queryService: QueryService, private userFollowingService: UserFollowingService, private metaService: MetaService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -92,10 +99,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.stlDisabled); } + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { - return await this.getFromDb({ + const timeline = await this.getFromDb({ untilId, sinceId, limit: ps.limit, @@ -106,106 +115,63 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- withReplies: ps.withReplies, withBots: ps.withBots, }, me); - } - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]); + process.nextTick(() => { + this.activeUsersChart.read(me); + }); + + return await this.noteEntityService.packMany(timeline, me); + } - let noteIds: string[]; - let shouldFallbackToDb = false; + let timelineConfig: FanoutTimelineName[]; if (ps.withFiles) { - const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ + timelineConfig = [ `homeTimelineWithFiles:${me.id}`, 'localTimelineWithFiles', - ], untilId, sinceId); - noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); + ]; } else if (ps.withReplies) { - const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([ + timelineConfig = [ `homeTimeline:${me.id}`, 'localTimeline', 'localTimelineWithReplies', - ], untilId, sinceId); - noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds])); + ]; } else { - const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ + timelineConfig = [ `homeTimeline:${me.id}`, 'localTimeline', - ], untilId, sinceId); - noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); - shouldFallbackToDb = htlNoteIds.length === 0; + ]; } - noteIds.sort((a, b) => a > b ? -1 : 1); - noteIds = noteIds.slice(0, ps.limit); - - shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0); - - let redisTimeline: MiNote[] = []; - - if (!shouldFallbackToDb) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - redisTimeline = await query.getMany(); - - redisTimeline = redisTimeline.filter(note => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } - - if (!ps.withBots && note.user?.isBot) return false; - - return true; - }); - - redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); - } + const redisTimeline = await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + redisTimelines: timelineConfig, + useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, + excludeBots: !ps.withBots, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + untilId, + sinceId, + limit, + includeMyRenotes: ps.includeMyRenotes, + includeRenotedMyNotes: ps.includeRenotedMyNotes, + includeLocalRenotes: ps.includeLocalRenotes, + withFiles: ps.withFiles, + withReplies: ps.withReplies, + withBots: ps.withBots, + }, me), + }); - if (redisTimeline.length > 0) { - process.nextTick(() => { - this.activeUsersChart.read(me); - }); + process.nextTick(() => { + this.activeUsersChart.read(me); + }); - return await this.noteEntityService.packMany(redisTimeline, me); - } else { - if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db - return await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withReplies: ps.withReplies, - withBots: ps.withBots, - }, me); - } else { - return []; - } - } + return redisTimeline; }); } @@ -309,12 +275,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (!ps.withBots) query.andWhere('user.isBot = FALSE'); //#endregion - const timeline = await query.limit(ps.limit).getMany(); - - process.nextTick(() => { - this.activeUsersChart.read(me); - }); - - return await this.noteEntityService.packMany(timeline, me); + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 73566aa45d..8db7d25e03 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiNote, NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -13,11 +13,10 @@ import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -39,6 +38,12 @@ export const meta = { code: 'LTL_DISABLED', id: '45a6eb02-7695-4393-b023-dd3be9aaaefd', }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793', + }, }, } as const; @@ -49,10 +54,10 @@ export const paramDef = { withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, withBots: { type: 'boolean', default: true }, - excludeNsfw: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, }, @@ -70,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, private metaService: MetaService, ) { @@ -83,10 +88,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.ltlDisabled); } + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { - return await this.getFromDb({ + const timeline = await this.getFromDb({ untilId, sinceId, limit: ps.limit, @@ -94,90 +101,48 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- withReplies: ps.withReplies, withBots: ps.withBots, }, me); - } - - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]) : [new Set<string>(), new Set<string>(), new Set<string>()]; - - let noteIds: string[]; - - if (ps.withFiles) { - noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId); - } else { - const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([ - 'localTimeline', - 'localTimelineWithReplies', - ], untilId, sinceId); - noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds])); - noteIds.sort((a, b) => a > b ? -1 : 1); - } - - noteIds = noteIds.slice(0, ps.limit); - - let redisTimeline: MiNote[] = []; - - if (noteIds.length > 0) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - redisTimeline = await query.getMany(); - - redisTimeline = redisTimeline.filter(note => { - if (me && (note.userId === me.id)) { - return true; - } - if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false; - if (!ps.withBots && note.user?.isBot) return false; - if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } - - return true; - }); - redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); - } - - if (redisTimeline.length > 0) { process.nextTick(() => { if (me) { this.activeUsersChart.read(me); } }); - return await this.noteEntityService.packMany(redisTimeline, me); - } else { - if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db - return await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - withFiles: ps.withFiles, - withReplies: ps.withReplies, - withBots: ps.withBots, - }, me); - } else { - return []; - } + return await this.noteEntityService.packMany(timeline, me); } + + const timeline = await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + redisTimelines: + ps.withFiles ? ['localTimelineWithFiles'] + : ps.withReplies ? ['localTimeline', 'localTimelineWithReplies'] + : me ? ['localTimeline', `localTimelineWithReplyTo:${me.id}`] + : ['localTimeline'], + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, + excludeBots: !ps.withBots, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + untilId, + sinceId, + limit, + withFiles: ps.withFiles, + withReplies: ps.withReplies, + withBots: ps.withBots, + }, me), + }); + + process.nextTick(() => { + if (me) { + this.activeUsersChart.read(me); + } + }); + + return timeline; }); } @@ -221,14 +186,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (!ps.withBots) query.andWhere('user.isBot = FALSE'); - const timeline = await query.limit(ps.limit).getMany(); - - process.nextTick(() => { - if (me) { - this.activeUsersChart.read(me); - } - }); - - return await this.noteEntityService.packMany(timeline, me); + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 036993508f..8e2de0cd37 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiNote, NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; +import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -13,11 +13,10 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MiLocalUser } from '@/models/User.js'; import { MetaService } from '@/core/MetaService.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; export const meta = { tags: ['notes'], @@ -43,6 +42,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, @@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private userFollowingService: UserFollowingService, private queryService: QueryService, private metaService: MetaService, @@ -78,7 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { - return await this.getFromDb({ + const timeline = await this.getFromDb({ untilId, sinceId, limit: ps.limit, @@ -89,83 +89,56 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- withRenotes: ps.withRenotes, withBots: ps.withBots, }, me); + + process.nextTick(() => { + this.activeUsersChart.read(me); + }); + + return await this.noteEntityService.packMany(timeline, me); } const [ followings, - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, ] = await Promise.all([ this.cacheService.userFollowingsCache.fetch(me.id), - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), ]); - let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId); - noteIds = noteIds.slice(0, ps.limit); - - let redisTimeline: MiNote[] = []; - - if (noteIds.length > 0) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - redisTimeline = await query.getMany(); - - redisTimeline = redisTimeline.filter(note => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } + const timeline = this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`], + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, + noteFilter: note => { if (note.reply && note.reply.visibility === 'followers') { if (!Object.hasOwn(followings, note.reply.userId)) return false; } if (!ps.withBots && note.user?.isBot) return false; return true; - }); - - redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); - } + }, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + untilId, + sinceId, + limit, + includeMyRenotes: ps.includeMyRenotes, + includeRenotedMyNotes: ps.includeRenotedMyNotes, + includeLocalRenotes: ps.includeLocalRenotes, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + withBots: ps.withBots, + }, me), + }); - if (redisTimeline.length > 0) { - process.nextTick(() => { - this.activeUsersChart.read(me); - }); + process.nextTick(() => { + this.activeUsersChart.read(me); + }); - return await this.noteEntityService.packMany(redisTimeline, me); - } else { - if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db - return await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, - withBots: ps.withBots, - }, me); - } else { - return []; - } - } + return timeline; }); } @@ -275,12 +248,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (!ps.withBots) query.andWhere('user.isBot = FALSE'); //#endregion - const timeline = await query.limit(ps.limit).getMany(); - - process.nextTick(() => { - this.activeUsersChart.read(me); - }); - - return await this.noteEntityService.packMany(timeline, me); + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index dbc3875597..10d3a7a697 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -5,18 +5,17 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import type { MiNote, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; +import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { MiLocalUser } from '@/models/User.js'; import { MetaService } from '@/core/MetaService.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -52,6 +51,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, @@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private activeUsersChart: ActiveUsersChart, private cacheService: CacheService, private idService: IdService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, private metaService: MetaService, ) { @@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { - return await this.getFromDb(list, { + const timeline = await this.getFromDb(list, { untilId, sinceId, limit: ps.limit, @@ -111,73 +111,37 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, }, me); - } - - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]); - - let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId); - noteIds = noteIds.slice(0, ps.limit); - - let redisTimeline: MiNote[] = []; - - if (noteIds.length > 0) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - redisTimeline = await query.getMany(); + this.activeUsersChart.read(me); - redisTimeline = redisTimeline.filter(note => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } + await this.noteEntityService.packMany(timeline, me); + } - return true; - }); + const timeline = await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`], + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb(list, { + untilId, + sinceId, + limit, + includeMyRenotes: ps.includeMyRenotes, + includeRenotedMyNotes: ps.includeRenotedMyNotes, + includeLocalRenotes: ps.includeLocalRenotes, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + }, me), + }); - redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); - } + this.activeUsersChart.read(me); - if (redisTimeline.length > 0) { - this.activeUsersChart.read(me); - return await this.noteEntityService.packMany(redisTimeline, me); - } else { - if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db - return await this.getFromDb(list, { - untilId, - sinceId, - limit: ps.limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, - }, me); - } else { - return []; - } - } + return timeline; }); } @@ -271,10 +235,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } //#endregion - const timeline = await query.limit(ps.limit).getMany(); - - this.activeUsersChart.read(me); - - return await this.noteEntityService.packMany(timeline, me); + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts index d1de73ad32..dc2be8e11d 100644 --- a/packages/backend/src/server/api/endpoints/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/roles/list.ts @@ -13,6 +13,16 @@ export const meta = { tags: ['role'], requireCredential: true, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Role', + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index daa9affc20..7010df22c9 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -11,7 +11,7 @@ import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- return []; } - let noteIds = await this.funoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId); + let noteIds = await this.fanoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId); noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { diff --git a/packages/backend/src/server/api/endpoints/roles/show.ts b/packages/backend/src/server/api/endpoints/roles/show.ts index 2afa0e7b7f..6bfe52bb1a 100644 --- a/packages/backend/src/server/api/endpoints/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/roles/show.ts @@ -22,6 +22,12 @@ export const meta = { id: 'de5502bf-009a-4639-86c1-fec349e46dcb', }, }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'Role', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index caaa3735e9..d304d075b2 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -24,6 +24,25 @@ export const meta = { id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5', }, }, + + res: { + type: 'array', + items: { + type: 'object', + nullable: false, + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + user: { + type: 'object', + ref: 'User' + }, + }, + required: ['id', 'user'], + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index c8cb63e6b3..079f2d7f1d 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -15,6 +15,53 @@ export const meta = { cacheSec: 60 * 1, tags: ['meta'], + res: { + type: 'object', + optional: false, nullable: false, + properties: { + machine: { + type: 'string', + nullable: false, + }, + cpu: { + type: 'object', + nullable: false, + properties: { + model: { + type: 'string', + nullable: false, + }, + cores: { + type: 'number', + nullable: false, + }, + }, + }, + mem: { + type: 'object', + properties: { + total: { + type: 'number', + nullable: false, + }, + }, + }, + fs: { + type: 'object', + nullable: false, + properties: { + total: { + type: 'number', + nullable: false, + }, + used: { + type: 'number', + nullable: false, + }, + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index 6d6d44f752..949867c572 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -12,6 +12,30 @@ export const meta = { description: 'Endpoint for testing input validation.', requireCredential: false, + + res: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + required: { + type: 'boolean', + }, + string: { + type: 'string', + }, + default: { + type: 'string', + }, + nullableDefault: { + type: 'string', + default: 'hello', + nullable: true, + }, + } + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts index e4845d57bf..d6ad718dfa 100644 --- a/packages/backend/src/server/api/endpoints/users/achievements.ts +++ b/packages/backend/src/server/api/endpoints/users/achievements.ts @@ -10,6 +10,21 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + unlockedAt: { + type: 'number', + }, + }, + }, + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/featured-notes.ts b/packages/backend/src/server/api/endpoints/users/featured-notes.ts index dec0b7a122..7243aa3b3e 100644 --- a/packages/backend/src/server/api/endpoints/users/featured-notes.ts +++ b/packages/backend/src/server/api/endpoints/users/featured-notes.ts @@ -9,6 +9,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { FeaturedService } from '@/core/FeaturedService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; export const meta = { tags: ['notes'], @@ -46,8 +48,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteEntityService: NoteEntityService, private featuredService: FeaturedService, + private cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { + const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set<string>(); + + // early return if me is blocked by requesting user + if (userIdsWhoBlockingMe.has(ps.userId)) { + return []; + } + let noteIds = await this.featuredService.getPerUserNotesRanking(ps.userId, 50); noteIds.sort((a, b) => a > b ? -1 : 1); @@ -60,6 +70,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- return []; } + const [ + userIdsWhoMeMuting, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + ]) : [new Set<string>()]; + const query = this.notesRepository.createQueryBuilder('note') .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') @@ -69,10 +85,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); - const notes = await query.getMany(); - notes.sort((a, b) => a.id > b.id ? -1 : 1); + const notes = (await query.getMany()).filter(note => { + if (me && isUserRelated(note, userIdsWhoBlockingMe, false)) return false; + if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false; - // TODO: ミュート等考慮 + return true; + }); + + notes.sort((a, b) => a.id > b.id ? -1 : 1); return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index b22fd2ff7a..5706e46b96 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -93,11 +93,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.ffVisibility === 'private') { + if (profile.followersVisibility === 'private') { if (me == null || (me.id !== user.id)) { throw new ApiError(meta.errors.forbidden); } - } else if (profile.ffVisibility === 'followers') { + } else if (profile.followersVisibility === 'followers') { if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 03487275a3..794fb04f10 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -42,6 +42,12 @@ export const meta = { code: 'FORBIDDEN', id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba', }, + + birthdayInvalid: { + message: 'Birthday date format is invalid.', + code: 'BIRTHDAY_DATE_FORMAT_INVALID', + id: 'a2b007b9-4782-4eba-abd3-93b05ed4130d', + }, }, } as const; @@ -59,6 +65,8 @@ export const paramDef = { nullable: true, description: 'The local host is represented with `null`.', }, + + birthday: { type: 'string', nullable: true }, }, anyOf: [ { required: ['userId'] }, @@ -93,11 +101,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.ffVisibility === 'private') { + if (profile.followingVisibility === 'private') { if (me == null || (me.id !== user.id)) { throw new ApiError(meta.errors.forbidden); } - } else if (profile.ffVisibility === 'followers') { + } else if (profile.followingVisibility === 'followers') { if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { @@ -117,6 +125,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .andWhere('following.followerId = :userId', { userId: user.id }) .innerJoinAndSelect('following.followee', 'followee'); + if (ps.birthday) { + try { + const d = new Date(ps.birthday); + d.setHours(0, 0, 0, 0); + const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`; + const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile'); + birthdayUserQuery.select('user_profile.userId') + .where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`); + + query.andWhere(`following.followeeId IN (${ birthdayUserQuery.getQuery() })`); + } catch (err) { + throw new ApiError(meta.errors.birthdayInvalid); + } + } + const followings = await query .limit(ps.limit) .getMany(); diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts index ae8b4e9b81..985141515e 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts @@ -25,6 +25,35 @@ export const meta = { id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686', }, }, + + res: { + type: 'array', + items: { + type: 'object', + nullable: false, + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + createdAt: { + type: 'string', + format: 'date-time', + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + user: { + type: 'object', + ref: 'User', + }, + withReplies: { + type: 'boolean', + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index c3d72c3ba8..b485126ed8 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -5,17 +5,18 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import type { MiNote, NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { QueryService } from '@/core/QueryService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; -import { ApiError } from '../../error.js'; +import { MetaService } from '@/core/MetaService.js'; +import { MiLocalUser } from '@/models/User.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; +import { ApiError } from '@/server/api/error.js'; export const meta = { tags: ['users', 'notes'], @@ -36,6 +37,12 @@ export const meta = { code: 'NO_SUCH_USER', id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b', }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222', + }, }, } as const; @@ -51,8 +58,8 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default withFiles: { type: 'boolean', default: false }, - excludeNsfw: { type: 'boolean', default: false }, }, required: ['userId'], } as const; @@ -60,9 +67,6 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -70,125 +74,130 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private queryService: QueryService, private cacheService: CacheService, private idService: IdService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, + private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const isRangeSpecified = untilId != null && sinceId != null; const isSelf = me && (me.id === ps.userId); - if (isRangeSpecified || sinceId == null) { - const [ - userIdsWhoMeMuting, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - ]) : [new Set<string>()]; - - const [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([ - this.funoutTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId), - ps.withReplies ? this.funoutTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), - ps.withChannelNotes ? this.funoutTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), - ]); - - let noteIds = Array.from(new Set([ - ...noteIdsRes, - ...repliesNoteIdsRes, - ...channelNoteIdsRes, - ])); - noteIds.sort((a, b) => a > b ? -1 : 1); - noteIds = noteIds.slice(0, ps.limit); + const serverSettings = await this.metaService.fetch(); - if (noteIds.length > 0) { - const isFollowing = me && Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId); + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - let timeline = await query.getMany(); - - timeline = timeline.filter(note => { - if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false; + // early return if me is blocked by requesting user + if (me != null) { + const userIdsWhoBlockingMe = await this.cacheService.userBlockedCache.fetch(me.id); + if (userIdsWhoBlockingMe.has(ps.userId)) { + return []; + } + } - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (ps.withRenotes === false) return false; - } - } + if (!serverSettings.enableFanoutTimeline) { + const timeline = await this.getFromDb({ + untilId, + sinceId, + limit: ps.limit, + userId: ps.userId, + withChannelNotes: ps.withChannelNotes, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + }, me); - if (ps.withFiles && note.fileIds.length === 0) return false; - if (!ps.withReplies && note.replyId) return false; + return await this.noteEntityService.packMany(timeline, me); + } - if (note.channel?.isSensitive && !isSelf) return false; - if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; - if (note.visibility === 'followers' && !isFollowing && !isSelf) return false; + const redisTimelines: FanoutTimelineName[] = [ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`]; - return true; - }); + if (ps.withReplies) redisTimelines.push(`userTimelineWithReplies:${ps.userId}`); + if (ps.withChannelNotes) redisTimelines.push(`userTimelineWithChannel:${ps.userId}`); - // TODO: フィルタで件数が減った場合の埋め合わせ処理 + const isFollowing = me && Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId); - timeline.sort((a, b) => a.id > b.id ? -1 : 1); + const timeline = await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + redisTimelines, + useDbFallback: true, + ignoreAuthorFromMute: true, + excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies + excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files + excludePureRenotes: !ps.withRenotes, + noteFilter: note => { + if (note.channel?.isSensitive && !isSelf) return false; + if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; + if (note.visibility === 'followers' && !isFollowing && !isSelf) return false; - if (timeline.length > 0) { - return await this.noteEntityService.packMany(timeline, me); - } - } - } + return true; + }, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + untilId, + sinceId, + limit, + userId: ps.userId, + withChannelNotes: ps.withChannelNotes, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + }, me), + }); - //#region fallback to database - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.userId = :userId', { userId: ps.userId }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('note.channel', 'channel') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); + return timeline; + }); + } - if (ps.withChannelNotes) { - if (!isSelf) query.andWhere(new Brackets(qb => { - qb.orWhere('note.channelId IS NULL'); - qb.orWhere('channel.isSensitive = false'); - })); - } else { - query.andWhere('note.channelId IS NULL'); - } + private async getFromDb(ps: { + untilId: string | null, + sinceId: string | null, + limit: number, + userId: string, + withChannelNotes: boolean, + withFiles: boolean, + withRenotes: boolean, + }, me: MiLocalUser | null) { + const isSelf = me && (me.id === ps.userId); - this.queryService.generateVisibilityQuery(query, me); - if (me) { - this.queryService.generateMutedUserQuery(query, me, { id: ps.userId }); - this.queryService.generateBlockedUserQuery(query, me); - } + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.userId = :userId', { userId: ps.userId }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('note.channel', 'channel') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } + if (ps.withChannelNotes) { + if (!isSelf) query.andWhere(new Brackets(qb => { + qb.orWhere('note.channelId IS NULL'); + qb.orWhere('channel.isSensitive = false'); + })); + } else { + query.andWhere('note.channelId IS NULL'); + } - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :userId', { userId: ps.userId }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + this.queryService.generateVisibilityQuery(query, me); + if (me) { + this.queryService.generateMutedUserQuery(query, me, { id: ps.userId }); + this.queryService.generateBlockedUserQuery(query, me); + } - if (!ps.withReplies) { - query.andWhere('note.replyId IS NULL'); - } + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } - const timeline = await query.limit(ps.limit).getMany(); + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.userId != :userId', { userId: ps.userId }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } - return await this.noteEntityService.packMany(timeline, me); - //#endregion - }); + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 4f972d3f7e..0e71510b48 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -4,7 +4,7 @@ */ import type { Config } from '@/config.js'; -import endpoints from '../endpoints.js'; +import endpoints, { IEndpoint } from '../endpoints.js'; import { errors as basicErrors } from './errors.js'; import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; @@ -33,16 +33,17 @@ export function genOpenapiSpec(config: Config) { schemas: schemas, securitySchemes: { - ApiKeyAuth: { - type: 'apiKey', - in: 'body', - name: 'i', + bearerAuth: { + type: 'http', + scheme: 'bearer', }, }, }, }; - for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { + // 書き換えたりするのでディープコピーしておく。そのまま編集するとメモリ上の値が汚れて次回以降の出力に影響する + const copiedEndpoints = JSON.parse(JSON.stringify(endpoints)) as IEndpoint[]; + for (const endpoint of copiedEndpoints) { const errors = {} as any; if (endpoint.meta.errors) { @@ -58,6 +59,11 @@ export function genOpenapiSpec(config: Config) { const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; + + if (endpoint.meta.secure) { + desc += '**Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.\n'; + } + desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; if (endpoint.meta.kind) { const kind = endpoint.meta.kind; @@ -79,6 +85,13 @@ export function genOpenapiSpec(config: Config) { schema.required = [...schema.required ?? [], 'file']; } + if (schema.required && schema.required.length <= 0) { + // 空配列は許可されない + schema.required = undefined; + } + + const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1); + const info = { operationId: endpoint.name, summary: endpoint.name, @@ -92,17 +105,19 @@ export function genOpenapiSpec(config: Config) { } : {}), ...(endpoint.meta.requireCredential ? { security: [{ - ApiKeyAuth: [], + bearerAuth: [], }], } : {}), - requestBody: { - required: true, - content: { - [requestType]: { - schema, + ...(hasBody ? { + requestBody: { + required: true, + content: { + [requestType]: { + schema, + }, }, }, - }, + } : {}), responses: { ...(endpoint.meta.res ? { '200': { @@ -118,6 +133,11 @@ export function genOpenapiSpec(config: Config) { description: 'OK (without any results)', }, }), + ...(endpoint.meta.res?.optional === true || endpoint.meta.res?.nullable === true ? { + '204': { + description: 'OK (without any results)', + }, + } : {}), '400': { description: 'Client error', content: { @@ -190,6 +210,7 @@ export function genOpenapiSpec(config: Config) { }; spec.paths['/' + endpoint.name] = { + ...(endpoint.meta.allowGet ? { get: info } : {}), post: info, }; } diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index 1a1d973e56..2716f5f162 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -7,10 +7,16 @@ import type { Schema } from '@/misc/json-schema.js'; import { refs } from '@/misc/json-schema.js'; export function convertSchemaToOpenApiSchema(schema: Schema) { - const res: any = schema; + // optional, refはスキーマ定義に含まれないので分離しておく + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { optional, ref, ...res }: any = schema; if (schema.type === 'object' && schema.properties) { - res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); + const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); + if (required.length > 0) { + // 空配列は許可されない + res.required = required; + } for (const k of Object.keys(schema.properties)) { res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]); diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index 2d8fec30b1..4180ccc56a 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -36,6 +36,7 @@ export default class Connection { public userIdsWhoMeMuting: Set<string> = new Set(); public userIdsWhoBlockingMe: Set<string> = new Set(); public userIdsWhoMeMutingRenotes: Set<string> = new Set(); + public userMutedInstances: Set<string> = new Set(); private fetchIntervalId: NodeJS.Timeout | null = null; constructor( @@ -69,6 +70,7 @@ export default class Connection { this.userIdsWhoMeMuting = userIdsWhoMeMuting; this.userIdsWhoBlockingMe = userIdsWhoBlockingMe; this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes; + this.userMutedInstances = new Set(userProfile.mutedInstances); } @bindThis diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 3aa0d69c0b..46b0709773 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -41,6 +41,10 @@ export default abstract class Channel { return this.connection.userIdsWhoBlockingMe; } + protected get userMutedInstances() { + return this.connection.userMutedInstances; + } + protected get followingChannels() { return this.connection.followingChannels; } 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 4b6628df6f..fe293e2b4d 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -5,12 +5,12 @@ import { Inject, Injectable } from '@nestjs/common'; import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import Channel from '../channel.js'; class UserListChannel extends Channel { @@ -80,6 +80,9 @@ class UserListChannel extends Channel { private async onNote(note: Packed<'Note'>) { const isMe = this.user!.id === note.userId; + // チャンネル投稿は無視する + if (note.channelId) return; + if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; if (!Object.hasOwn(this.membershipsMap, note.userId)) return; @@ -115,6 +118,9 @@ class UserListChannel extends Channel { } } + // 流れてきたNoteがミュートしているインスタンスに関わるものだったら無視する + if (isInstanceMuted(note, this.userMutedInstances)) return; + this.connection.cacheNote(note); this.send('note', note); diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index 94d0040313..aaa9566a76 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -58,9 +58,9 @@ export class FeedService { const feed = new Feed({ id: author.link, title: `${author.name} (@${user.username}@${this.config.host})`, - updated: this.idService.parse(notes[0].id).date, + updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined, generator: 'Sharkey', - description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, + description: `${user.notesCount} Notes, ${profile.followingVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.followersVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, link: author.link, image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), feedLinks: { diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index f88a1d4e56..e55952f296 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -14,18 +14,34 @@ * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された + * roleAssigned - ロールが付与された * achievementEarned - 実績を獲得 * app - アプリ通知 * test - テスト通知(サーバー側) */ -export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const; +export const notificationTypes = [ + 'note', + 'follow', + 'mention', + 'reply', + 'renote', + 'quote', + 'reaction', + 'pollEnded', + 'receiveFollowRequest', + 'followRequestAccepted', + 'roleAssigned', + 'achievementEarned', + 'app', + 'test'] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; -export const ffVisibility = ['public', 'followers', 'private'] as const; +export const followingVisibilities = ['public', 'followers', 'private'] as const; +export const followersVisibilities = ['public', 'followers', 'private'] as const; export const moderationLogTypes = [ 'updateServerSettings', @@ -64,6 +80,8 @@ export const moderationLogTypes = [ 'createAvatarDecoration', 'updateAvatarDecoration', 'deleteAvatarDecoration', + 'unsetUserAvatar', + 'unsetUserBanner', ] as const; export type ModerationLogPayloads = { @@ -243,6 +261,18 @@ export type ModerationLogPayloads = { avatarDecorationId: string; avatarDecoration: any; }; + unsetUserAvatar: { + userId: string; + userUsername: string; + userHost: string | null; + fileId: string; + }; + unsetUserBanner: { + userId: string; + userUsername: string; + userHost: string | null; + fileId: string; + }; }; export type Serialized<T> = { diff --git a/packages/backend/test/docker-compose.yml b/packages/backend/test/docker-compose.yml index da6c01dda1..f2d8990758 100644 --- a/packages/backend/test/docker-compose.yml +++ b/packages/backend/test/docker-compose.yml @@ -7,7 +7,7 @@ services: - "127.0.0.1:56312:6379" dbtest: - image: postgres:13 + image: postgres:15 ports: - "127.0.0.1:54312:5432" environment: diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 1cbfec3e5f..251d662760 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -93,7 +93,7 @@ describe('Webリソース', () => { }); aliceChannel = await channel(alice, {}); - bob = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); afterAll(async () => { @@ -152,6 +152,11 @@ describe('Webリソース', () => { type, })); + test('がGETできる。(ノートが存在しない場合でも。)', async () => await ok({ + path: path(bob.username), + type, + })); + test('は存在しないユーザーはGETできない。', async () => await notOk({ path: path('nonexisting'), status: 404, diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index 7841e057bf..1fbd45c741 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -26,9 +26,10 @@ describe('FF visibility', () => { await app.close(); }); - test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { + test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { await api('/i/update', { - ffVisibility: 'public', + followingVisibility: 'public', + followersVisibility: 'public', }, alice); const followingRes = await api('/users/following', { @@ -44,9 +45,88 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { + test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + }); + + test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'public', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'public', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'public', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + }); + + test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { await api('/i/update', { - ffVisibility: 'followers', + followingVisibility: 'followers', + followersVisibility: 'followers', }, alice); const followingRes = await api('/users/following', { @@ -62,9 +142,88 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { + test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + }); + + test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + }); + + test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { await api('/i/update', { - ffVisibility: 'followers', + followingVisibility: 'followers', + followersVisibility: 'followers', }, alice); const followingRes = await api('/users/following', { @@ -78,9 +237,82 @@ describe('FF visibility', () => { assert.strictEqual(followersRes.status, 400); }); - test('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { + test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => { + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + }); + + test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + }); + + test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { await api('/i/update', { - ffVisibility: 'followers', + followingVisibility: 'followers', + followersVisibility: 'followers', }, alice); await api('/following/create', { @@ -100,9 +332,106 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => { + test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'public', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + }); + + test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'followers', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + }); + + test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => { await api('/i/update', { - ffVisibility: 'private', + followingVisibility: 'private', + followersVisibility: 'private', }, alice); const followingRes = await api('/users/following', { @@ -118,9 +447,88 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => { + test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + }); + + test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + }); + + test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => { await api('/i/update', { - ffVisibility: 'private', + followingVisibility: 'private', + followersVisibility: 'private', }, alice); const followingRes = await api('/users/following', { @@ -134,36 +542,129 @@ describe('FF visibility', () => { assert.strictEqual(followersRes.status, 400); }); + test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => { + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + }); + + test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + }); + describe('AP', () => { - test('ffVisibility が public 以外ならばAPからは取得できない', async () => { + test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => { { await api('/i/update', { - ffVisibility: 'public', + followingVisibility: 'public', }, alice); const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); assert.strictEqual(followingRes.status, 200); - assert.strictEqual(followersRes.status, 200); } { await api('/i/update', { - ffVisibility: 'followers', + followingVisibility: 'followers', }, alice); const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); assert.strictEqual(followingRes.status, 403); - assert.strictEqual(followersRes.status, 403); } { await api('/i/update', { - ffVisibility: 'private', + followingVisibility: 'private', }, alice); const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); assert.strictEqual(followingRes.status, 403); + } + }); + + test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => { + { + await api('/i/update', { + followersVisibility: 'public', + }, alice); + + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followersRes.status, 200); + } + { + await api('/i/update', { + followersVisibility: 'followers', + }, alice); + + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followersRes.status, 403); + } + { + await api('/i/update', { + followersVisibility: 'private', + }, alice); + + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); assert.strictEqual(followersRes.status, 403); } }); diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index f9f385e2b2..c4824f50ce 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -7,7 +7,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { MiFollowing } from '@/models/Following.js'; -import { connectStream, signup, api, post, startServer, initTestDb, waitFire } from '../utils.js'; +import { signup, api, post, startServer, initTestDb, waitFire } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; @@ -34,12 +34,16 @@ describe('Streaming', () => { let ayano: misskey.entities.MeSignup; let kyoko: misskey.entities.MeSignup; let chitose: misskey.entities.MeSignup; + let kanako: misskey.entities.MeSignup; // Remote users let akari: misskey.entities.MeSignup; let chinatsu: misskey.entities.MeSignup; + let takumi: misskey.entities.MeSignup; let kyokoNote: any; + let kanakoNote: any; + let takumiNote: any; let list: any; beforeAll(async () => { @@ -50,11 +54,15 @@ describe('Streaming', () => { ayano = await signup({ username: 'ayano' }); kyoko = await signup({ username: 'kyoko' }); chitose = await signup({ username: 'chitose' }); + kanako = await signup({ username: 'kanako' }); akari = await signup({ username: 'akari', host: 'example.com' }); chinatsu = await signup({ username: 'chinatsu', host: 'example.com' }); + takumi = await signup({ username: 'takumi', host: 'example.com' }); kyokoNote = await post(kyoko, { text: 'foo' }); + kanakoNote = await post(kanako, { text: 'hoge' }); + takumiNote = await post(takumi, { text: 'piyo' }); // Follow: ayano => kyoko await api('following/create', { userId: kyoko.id }, ayano); @@ -62,6 +70,9 @@ describe('Streaming', () => { // Follow: ayano => akari await follow(ayano, akari); + // Mute: chitose => kanako + await api('mute/create', { userId: kanako.id }, chitose); + // List: chitose => ayano, kyoko list = await api('users/lists/create', { name: 'my list', @@ -76,6 +87,11 @@ describe('Streaming', () => { listId: list.id, userId: kyoko.id, }, chitose); + + await api('users/lists/push', { + listId: list.id, + userId: takumi.id, + }, chitose); }, 1000 * 60 * 2); afterAll(async () => { @@ -452,6 +468,96 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); + + // #10443 + test('チャンネル投稿は流れない', async () => { + // リスインしている kyoko が 任意のチャンネルに投降した時の動きを見たい + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo', channelId: 'dummy' }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + + // #10443 + test('ミュートしているユーザへのリプライがリストTLに流れない', async () => { + // chitose が kanako をミュートしている状態で、リスインしている kyoko が kanako にリプライした時の動きを見たい + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo', replyId: kanakoNote.id }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + + // #10443 + test('ミュートしているユーザの投稿をリノートしたときリストTLに流れない', async () => { + // chitose が kanako をミュートしている状態で、リスインしている kyoko が kanako のノートをリノートした時の動きを見たい + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { renoteId: kanakoNote.id }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + + // #10443 + test('ミュートしているサーバのノートがリストTLに流れない', async () => { + await api('/i/update', { + mutedInstances: ['example.com'], + }, chitose); + + // chitose が example.com をミュートしている状態で、リスインしている takumi が ノートした時の動きを見たい + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo' }, takumi), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + + // #10443 + test('ミュートしているサーバのノートに対するリプライがリストTLに流れない', async () => { + await api('/i/update', { + mutedInstances: ['example.com'], + }, chitose); + + // chitose が example.com をミュートしている状態で、リスインしている kyoko が takumi のノートにリプライした時の動きを見たい + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo', replyId: takumiNote.id }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + + // #10443 + test('ミュートしているサーバのノートに対するリノートがリストTLに流れない', async () => { + await api('/i/update', { + mutedInstances: ['example.com'], + }, chitose); + + // chitose が example.com をミュートしている状態で、リスインしている kyoko が takumi のノートをリノートした時の動きを見たい + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { renoteId: takumiNote.id }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); }); // XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac" diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index 73c446444b..cb9558b416 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -10,9 +10,8 @@ process.env.NODE_ENV = 'test'; process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true'; import * as assert from 'assert'; -import { signup, api, post, react, startServer, waitFire, sleep, uploadUrl, randomString } from '../utils.js'; +import { api, post, randomString, signup, sleep, startServer, uploadUrl } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; -import type * as misskey from 'misskey-js'; function genHost() { return randomString() + '.example.com'; @@ -366,8 +365,8 @@ describe('Timelines', () => { await api('/following/create', { userId: bob.id }, alice); await sleep(1000); const [bobFile, carolFile] = await Promise.all([ - uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'), - uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'), + uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), + uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), ]); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [bobFile.id] }); @@ -666,7 +665,7 @@ describe('Timelines', () => { test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); @@ -804,7 +803,7 @@ describe('Timelines', () => { test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); @@ -999,7 +998,7 @@ describe('Timelines', () => { const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); @@ -1158,7 +1157,7 @@ describe('Timelines', () => { test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 67b2a36ec2..be6f0ec855 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -115,7 +115,8 @@ describe('ユーザー', () => { pinnedPageId: user.pinnedPageId, pinnedPage: user.pinnedPage, publicReactions: user.publicReactions, - ffVisibility: user.ffVisibility, + followingVisibility: user.followingVisibility, + followersVisibility: user.followersVisibility, twoFactorEnabled: user.twoFactorEnabled, usePasswordLessLogin: user.usePasswordLessLogin, securityKeys: user.securityKeys, @@ -171,6 +172,7 @@ describe('ユーザー', () => { hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest, unreadAnnouncements: user.unreadAnnouncements, mutedWords: user.mutedWords, + hardMutedWords: user.hardMutedWords, mutedInstances: user.mutedInstances, mutingNotificationTypes: user.mutingNotificationTypes, notificationRecieveConfig: user.notificationRecieveConfig, @@ -391,7 +393,8 @@ describe('ユーザー', () => { assert.strictEqual(response.pinnedPageId, null); assert.strictEqual(response.pinnedPage, null); assert.strictEqual(response.publicReactions, true); - assert.strictEqual(response.ffVisibility, 'public'); + assert.strictEqual(response.followingVisibility, 'public'); + assert.strictEqual(response.followersVisibility, 'public'); assert.strictEqual(response.twoFactorEnabled, false); assert.strictEqual(response.usePasswordLessLogin, false); assert.strictEqual(response.securityKeys, false); @@ -502,9 +505,12 @@ describe('ユーザー', () => { { parameters: (): object => ({ alwaysMarkNsfw: false }) }, { parameters: (): object => ({ autoSensitive: true }) }, { parameters: (): object => ({ autoSensitive: false }) }, - { parameters: (): object => ({ ffVisibility: 'private' }) }, - { parameters: (): object => ({ ffVisibility: 'followers' }) }, - { parameters: (): object => ({ ffVisibility: 'public' }) }, + { parameters: (): object => ({ followingVisibility: 'private' }) }, + { parameters: (): object => ({ followingVisibility: 'followers' }) }, + { parameters: (): object => ({ followingVisibility: 'public' }) }, + { parameters: (): object => ({ followersVisibility: 'private' }) }, + { parameters: (): object => ({ followersVisibility: 'followers' }) }, + { parameters: (): object => ({ followersVisibility: 'public' }) }, { parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) }, { parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) }, { parameters: (): object => ({ mutedWords: [] }) }, diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index f644312bc9..9879eb8e3e 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -19,6 +19,7 @@ import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { sleep } from '../utils.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -32,6 +33,7 @@ describe('RoleService', () => { let rolesRepository: RolesRepository; let roleAssignmentsRepository: RoleAssignmentsRepository; let metaService: jest.Mocked<MetaService>; + let notificationService: jest.Mocked<NotificationService>; let clock: lolex.InstalledClock; function createUser(data: Partial<MiUser> = {}) { @@ -71,6 +73,16 @@ describe('RoleService', () => { CacheService, IdService, GlobalEventService, + { + provide: NotificationService, + useFactory: () => ({ + createNotification: jest.fn(), + }), + }, + { + provide: NotificationService.name, + useExisting: NotificationService, + }, ], }) .useMocker((token) => { @@ -93,6 +105,9 @@ describe('RoleService', () => { roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository); metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>; + notificationService = app.get<NotificationService>(NotificationService) as jest.Mocked<NotificationService>; + + await roleService.onModuleInit(); }); afterEach(async () => { @@ -273,4 +288,57 @@ describe('RoleService', () => { expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true); }); }); + + describe('assign', () => { + test('公開ロールの場合は通知される', async () => { + const user = await createUser(); + const role = await createRole({ + isPublic: true, + name: 'a', + }); + + await roleService.assign(user.id, role.id); + + clock.uninstall(); + await sleep(100); + + const assignments = await roleAssignmentsRepository.find({ + where: { + userId: user.id, + roleId: role.id, + }, + }); + expect(assignments).toHaveLength(1); + + expect(notificationService.createNotification).toHaveBeenCalled(); + expect(notificationService.createNotification.mock.lastCall![0]).toBe(user.id); + expect(notificationService.createNotification.mock.lastCall![1]).toBe('roleAssigned'); + expect(notificationService.createNotification.mock.lastCall![2]).toEqual({ + roleId: role.id, + }); + }); + + test('非公開ロールの場合は通知されない', async () => { + const user = await createUser(); + const role = await createRole({ + isPublic: false, + name: 'a', + }); + + await roleService.assign(user.id, role.id); + + clock.uninstall(); + await sleep(100); + + const assignments = await roleAssignmentsRepository.find({ + where: { + userId: user.id, + roleId: role.id, + }, + }); + expect(assignments).toHaveLength(1); + + expect(notificationService.createNotification).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/frontend/.eslintrc.cjs b/packages/frontend/.eslintrc.cjs index 77038f0dfa..20f88dc078 100644 --- a/packages/frontend/.eslintrc.cjs +++ b/packages/frontend/.eslintrc.cjs @@ -69,12 +69,6 @@ module.exports = { 'require': false, '__dirname': false, - // Vue - '$$': false, - '$ref': false, - '$shallowRef': false, - '$computed': false, - // Misskey '_DEV_': false, '_LANGS_': false, diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index 71114d5838..fdf98665d0 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -82,7 +82,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi birthday: '2014-06-20', createdAt: '2016-12-28T22:49:51.000Z', description: 'I am a cool user!', - ffVisibility: 'public', + followingVisibility: 'public', + followersVisibility: 'public', roles: [], fields: [ { diff --git a/packages/frontend/.storybook/mocks.ts b/packages/frontend/.storybook/mocks.ts index b60755feea..80e5157c5a 100644 --- a/packages/frontend/.storybook/mocks.ts +++ b/packages/frontend/.storybook/mocks.ts @@ -25,7 +25,7 @@ export const commonHandlers = [ }), rest.get('/twemoji/:codepoints.svg', async (req, res, ctx) => { const { codepoints } = req.params; - const value = await fetch(`https://unpkg.com/@discordapp/twemoji@14.1.2/dist/svg/${codepoints}.svg`).then((response) => response.blob()); + const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob()); return res(ctx.set('Content-Type', 'image/svg+xml'), ctx.body(value)); }), ]; diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html index 36ff34b48a..30f3ebfb64 100644 --- a/packages/frontend/.storybook/preview-head.html +++ b/packages/frontend/.storybook/preview-head.html @@ -1,6 +1,6 @@ <link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous"> <link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous"> -<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.37.0/tabler-icons.min.css"> +<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.44.0/tabler-icons.min.css"> <link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css"> <style> html { diff --git a/packages/frontend/@types/global.d.ts b/packages/frontend/@types/global.d.ts index 390f63990b..7d9335cc52 100644 --- a/packages/frontend/@types/global.d.ts +++ b/packages/frontend/@types/global.d.ts @@ -13,3 +13,6 @@ declare const _PERF_PREFIX_: string; declare const _DATA_TRANSFER_DRIVE_FILE_: string; declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; declare const _DATA_TRANSFER_DECK_COLUMN_: string; + +// for dev-mode +declare const _LANGS_FULL_: string[][]; diff --git a/packages/frontend/assets/sounds/syuilo/bubble1.mp3 b/packages/frontend/assets/sounds/syuilo/bubble1.mp3 Binary files differnew file mode 100644 index 0000000000..05b8ef8b10 --- /dev/null +++ b/packages/frontend/assets/sounds/syuilo/bubble1.mp3 diff --git a/packages/frontend/assets/sounds/syuilo/bubble2.mp3 b/packages/frontend/assets/sounds/syuilo/bubble2.mp3 Binary files differnew file mode 100644 index 0000000000..8b4f8df6e9 --- /dev/null +++ b/packages/frontend/assets/sounds/syuilo/bubble2.mp3 diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts index 3640b656b4..fab80eae2f 100644 --- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts @@ -89,7 +89,6 @@ const _sfc_main = /* @__PURE__ */ defineComponent({ api("users/notes", { userId: props.user.id, fileType: image, - excludeNsfw: defaultStore.state.nsfw !== "ignore", limit: 10 }).then((notes) => { for (const note of notes) { @@ -181,7 +180,7 @@ import './photoswipe-!~{003}~.js'; const _hoisted_1 = createBaseVNode("i", { class: "ph-image-square ph-bold ph-lg" }, null, -1); -const _sfc_main = defineComponent({ +const index_photos = defineComponent({ __name: "index.photos", props: { user: {} @@ -198,7 +197,6 @@ const _sfc_main = defineComponent({ api("users/notes", { userId: props.user.id, fileType: image, - excludeNsfw: defaultStore.state.nsfw !== "ignore", limit: 10 }).then(notes => { for (const note of notes) { @@ -263,7 +261,6 @@ const style0 = { const cssModules = { "$style": style0 }; -const index_photos = _sfc_main; export {index_photos as default}; `.slice(1)); }); diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts index 18c817e0f5..68cdc0bc78 100644 --- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts @@ -13,13 +13,13 @@ function isFalsyIdentifier(identifier: estree.Identifier): boolean { return identifier.name === 'undefined' || identifier.name === 'NaN'; } -function normalizeClassWalker(tree: estree.Node): string | null { +function normalizeClassWalker(tree: estree.Node, stack: string | undefined): string | null { if (tree.type === 'Identifier') return isFalsyIdentifier(tree) ? '' : null; if (tree.type === 'Literal') return typeof tree.value === 'string' ? tree.value : ''; if (tree.type === 'BinaryExpression') { if (tree.operator !== '+') return null; - const left = normalizeClassWalker(tree.left); - const right = normalizeClassWalker(tree.right); + const left = normalizeClassWalker(tree.left, stack); + const right = normalizeClassWalker(tree.right, stack); if (left === null || right === null) return null; return `${left}${right}`; } @@ -33,15 +33,15 @@ function normalizeClassWalker(tree: estree.Node): string | null { if (tree.type === 'ArrayExpression') { const values = tree.elements.map((treeNode) => { if (treeNode === null) return ''; - if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument); - return normalizeClassWalker(treeNode); + if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument, stack); + return normalizeClassWalker(treeNode, stack); }); if (values.some((x) => x === null)) return null; return values.join(' '); } if (tree.type === 'ObjectExpression') { const values = tree.properties.map((treeNode) => { - if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument); + if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument, stack); let x = treeNode.value; let inveted = false; while (x.type === 'UnaryExpression' && x.operator === '!') { @@ -67,18 +67,26 @@ function normalizeClassWalker(tree: estree.Node): string | null { if (values.some((x) => x === null)) return null; return values.join(' '); } - console.error(`Unexpected node type: ${tree.type}`); + if ( + tree.type !== 'CallExpression' && + tree.type !== 'ChainExpression' && + tree.type !== 'ConditionalExpression' && + tree.type !== 'LogicalExpression' && + tree.type !== 'MemberExpression') { + console.error(stack ? `Unexpected node type: ${tree.type} (in ${stack})` : `Unexpected node type: ${tree.type}`); + } return null; } -export function normalizeClass(tree: estree.Node): string | null { - const walked = normalizeClassWalker(tree); +export function normalizeClass(tree: estree.Node, stack?: string): string | null { + const walked = normalizeClassWalker(tree, stack); return walked && walked.replace(/^\s+|\s+(?=\s)|\s+$/g, ''); } export function unwindCssModuleClassName(ast: estree.Node): void { (walk as typeof estreeWalker.walk)(ast, { enter(node, parent): void { + //#region if (parent?.type !== 'Program') return; if (node.type !== 'VariableDeclaration') return; if (node.declarations.length !== 1) return; @@ -102,6 +110,14 @@ export function unwindCssModuleClassName(ast: estree.Node): void { return true; }); if (!~__cssModulesIndex) return; + /* This region assumeed that the entered node looks like the following code. + * + * ```ts + * const SomeComponent = _export_sfc(_sfc_main, [["foo", bar], ["__cssModules", cssModules]]); + * ``` + */ + //#endregion + //#region const cssModuleForestName = ((node.declarations[0].init.arguments[1].elements[__cssModulesIndex] as estree.ArrayExpression).elements[1] as estree.Identifier).name; const cssModuleForestNode = parent.body.find((x) => { if (x.type !== 'VariableDeclaration') return false; @@ -117,6 +133,16 @@ export function unwindCssModuleClassName(ast: estree.Node): void { if (property.value.type !== 'Identifier') return []; return [[property.key.value as string, property.value.name as string]]; })); + /* This region collected a VariableDeclaration node in the module that looks like the following code. + * + * ```ts + * const cssModules = { + * "$style": style0, + * }; + * ``` + */ + //#endregion + //#region const sfcMain = parent.body.find((x) => { if (x.type !== 'VariableDeclaration') return false; if (x.declarations.length !== 1) return false; @@ -146,7 +172,22 @@ export function unwindCssModuleClassName(ast: estree.Node): void { if (ctx.type !== 'Identifier') return; if (ctx.name !== '_ctx') return; if (render.argument.body.type !== 'BlockStatement') return; + /* This region assumed that `sfcMain` looks like the following code. + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * }; + * }, + * }); + * ``` + */ + //#endregion for (const [key, value] of moduleForest) { + //#region const cssModuleTreeNode = parent.body.find((x) => { if (x.type !== 'VariableDeclaration') return false; if (x.declarations.length !== 1) return false; @@ -172,6 +213,19 @@ export function unwindCssModuleClassName(ast: estree.Node): void { if (actualValue.declarations[0].init?.type !== 'Literal') return []; return [[actualKey, actualValue.declarations[0].init.value as string]]; })); + /* This region collected VariableDeclaration nodes in the module that looks like the following code. + * + * ```ts + * const foo = "bar"; + * const baz = "qux"; + * const style0 = { + * foo: foo, + * baz: baz, + * }; + * ``` + */ + //#endregion + //#region (walk as typeof estreeWalker.walk)(render.argument.body, { enter(childNode) { if (childNode.type !== 'MemberExpression') return; @@ -189,6 +243,39 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }); }, }); + /* This region inlined the reference identifier of the class name in the render function into the actual literal, as in the following code. + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass(_ctx.$style.foo), + * }, null); + * }; + * }, + * }); + * ``` + * + * ↓ + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass("bar"), + * }, null); + * }; + * }, + * }); + */ + //#endregion + //#region (walk as typeof estreeWalker.walk)(render.argument.body, { enter(childNode) { if (childNode.type !== 'MemberExpression') return; @@ -205,13 +292,47 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }); }, }); + /* This region replaced the reference identifier of missing class names in the render function with `undefined`, as in the following code. + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass(_ctx.$style.hoge), + * }, null); + * }; + * }, + * }); + * ``` + * + * ↓ + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass(undefined), + * }, null); + * }; + * }, + * }); + * ``` + */ + //#endregion + //#region (walk as typeof estreeWalker.walk)(render.argument.body, { enter(childNode) { if (childNode.type !== 'CallExpression') return; if (childNode.callee.type !== 'Identifier') return; if (childNode.callee.name !== 'normalizeClass') return; if (childNode.arguments.length !== 1) return; - const normalized = normalizeClass(childNode.arguments[0]); + const normalized = normalizeClass(childNode.arguments[0], name); if (normalized === null) return; this.replace({ type: 'Literal', @@ -219,8 +340,60 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }); }, }); + /* This region compiled the `normalizeClass` call into a pseudo-AOT compilation, as in the following code. + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass("bar"), + * }, null); + * }; + * }, + * }); + * ``` + * + * ↓ + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: "bar", + * }, null); + * }; + * }, + * }); + * ``` + */ + //#endregion } + //#region if (node.declarations[0].init.arguments[1].elements.length === 1) { + (walk as typeof estreeWalker.walk)(ast, { + enter(childNode) { + if (childNode.type !== 'Identifier') return; + if (childNode.name !== ident) return; + this.replace({ + type: 'Identifier', + name: node.declarations[0].id.name, + }); + }, + }); + this.remove(); + /* NOTE: The above logic is valid as long as the following two conditions are met. + * + * - the uniqueness of `ident` is kept throughout the module + * - `_export_sfc` is noop when the second argument is an empty array + * + * Otherwise, the below logic should be used instead. + this.replace({ type: 'VariableDeclaration', declarations: [{ @@ -236,6 +409,7 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }], kind: 'const', }); + */ } else { this.replace({ type: 'VariableDeclaration', @@ -263,6 +437,35 @@ export function unwindCssModuleClassName(ast: estree.Node): void { kind: 'const', }); } + /* This region removed the `__cssModules` reference from the second argument of `_export_sfc`, as in the following code. + * + * ```ts + * const SomeComponent = _export_sfc(_sfc_main, [["foo", bar], ["__cssModules", cssModules]]); + * ``` + * + * ↓ + * + * ```ts + * const SomeComponent = _export_sfc(_sfc_main, [["foo", bar]]); + * ``` + * + * When the declaration becomes noop, it is removed as follows. + * + * ```ts + * const _sfc_main = defineComponent({ + * ... + * }); + * const SomeComponent = _export_sfc(_sfc_main, []); + * ``` + * + * ↓ + * + * ```ts + * const SomeComponent = defineComponent({ + * ... + * }); + */ + //#endregion }, }); } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 69e3406f94..275a6ad025 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -4,138 +4,134 @@ "type": "module", "scripts": { "watch": "vite", + "dev": "vite --config vite.config.local-dev.ts", "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", "build-storybook": "pnpm build-storybook-pre && storybook build", "chromatic": "chromatic", - "test": "vitest --run", + "test": "vitest --run --globals", "test-and-coverage": "vitest --run --coverage --globals", "typecheck": "vue-tsc --noEmit", "eslint": "eslint --quiet \"src/**/*.{ts,vue}\"", "lint": "pnpm typecheck && pnpm eslint" }, "dependencies": { - "@discordapp/twemoji": "14.1.2", + "@discordapp/twemoji": "15.0.2", "@github/webauthn-json": "2.1.1", - "@phosphor-icons/web": "^2.0.3", - "@rollup/plugin-alias": "5.0.1", - "@rollup/plugin-json": "6.0.1", + "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "5.0.5", - "@rollup/pluginutils": "5.0.5", - "@sharkey/sfm-js": "0.23.3", + "@rollup/pluginutils": "5.1.0", + "@sharkey/sfm-js": "0.24.0", "@syuilo/aiscript": "0.16.0", - "@vitejs/plugin-vue": "4.5.0", - "@vue-macros/reactivity-transform": "0.4.0", - "@vue/compiler-sfc": "3.3.8", + "@phosphor-icons/web": "^2.0.3", + "@twemoji/parser": "15.0.0", + "@vitejs/plugin-vue": "4.5.2", + "@vue/compiler-sfc": "3.3.12", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6", "astring": "1.8.6", - "autosize": "6.0.1", - "broadcast-channel": "6.0.0", + "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.0", + "chart.js": "4.4.1", "chartjs-adapter-date-fns": "3.0.0", "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.0.1", - "chromatic": "9.0.0", + "chromatic": "10.1.0", "compare-versions": "6.1.0", "cropperjs": "2.0.0-beta.4", "date-fns": "2.30.0", "escape-regexp": "0.0.1", "estree-walker": "3.0.3", "eventemitter3": "5.0.1", - "gsap": "3.12.2", + "gsap": "3.12.4", "idb-keyval": "6.2.1", "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", "json5": "2.2.3", "matter-js": "0.19.0", "misskey-js": "workspace:*", - "photoswipe": "5.4.2", + "photoswipe": "5.4.3", "punycode": "2.3.1", - "querystring": "0.2.1", - "rollup": "4.4.1", + "rollup": "4.9.1", "sanitize-html": "2.11.0", "sass": "1.69.5", - "shiki": "^0.14.5", + "shiki": "0.14.7", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", - "three": "0.158.0", + "three": "0.159.0", "throttle-debounce": "5.0.0", "tinycolor2": "1.6.0", "tsc-alias": "1.8.8", "tsconfig-paths": "4.2.0", - "twemoji-parser": "14.0.0", - "typescript": "5.2.2", + "typescript": "5.3.3", "uuid": "9.0.1", "v-code-diff": "1.7.2", - "vanilla-tilt": "1.8.1", - "vite": "4.5.0", - "vue": "3.3.8", + "vite": "5.0.10", + "vue": "3.3.12", "vuedraggable": "next" }, "devDependencies": { - "@storybook/addon-actions": "7.5.3", - "@storybook/addon-essentials": "7.5.3", - "@storybook/addon-interactions": "7.5.3", - "@storybook/addon-links": "7.5.3", - "@storybook/addon-storysource": "7.5.3", - "@storybook/addons": "7.5.3", - "@storybook/blocks": "7.5.3", - "@storybook/core-events": "7.5.3", + "@storybook/addon-actions": "7.6.5", + "@storybook/addon-essentials": "7.6.5", + "@storybook/addon-interactions": "7.6.5", + "@storybook/addon-links": "7.6.5", + "@storybook/addon-storysource": "7.6.5", + "@storybook/addons": "7.6.5", + "@storybook/blocks": "7.6.5", + "@storybook/core-events": "7.6.5", "@storybook/jest": "0.2.3", - "@storybook/manager-api": "7.5.3", - "@storybook/preview-api": "7.5.3", - "@storybook/react": "7.5.3", - "@storybook/react-vite": "7.5.3", + "@storybook/manager-api": "7.6.5", + "@storybook/preview-api": "7.6.5", + "@storybook/react": "7.6.5", + "@storybook/react-vite": "7.6.5", "@storybook/testing-library": "0.2.2", - "@storybook/theming": "7.5.3", - "@storybook/types": "7.5.3", - "@storybook/vue3": "7.5.3", - "@storybook/vue3-vite": "7.5.3", - "@testing-library/vue": "8.0.0", + "@storybook/theming": "7.6.5", + "@storybook/types": "7.6.5", + "@storybook/vue3": "7.6.5", + "@storybook/vue3-vite": "7.6.5", + "@testing-library/vue": "8.0.1", "@types/escape-regexp": "0.0.3", "@types/estree": "1.0.5", - "@types/matter-js": "0.19.4", - "@types/micromatch": "4.0.5", - "@types/node": "20.9.1", - "@types/punycode": "2.1.2", - "@types/sanitize-html": "2.9.4", + "@types/matter-js": "0.19.5", + "@types/micromatch": "4.0.6", + "@types/node": "20.10.5", + "@types/punycode": "2.1.3", + "@types/sanitize-html": "2.9.5", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/uuid": "9.0.7", - "@types/websocket": "1.0.9", - "@types/ws": "8.5.9", - "@typescript-eslint/eslint-plugin": "6.11.0", - "@typescript-eslint/parser": "6.11.0", + "@types/ws": "8.5.10", + "@typescript-eslint/eslint-plugin": "6.14.0", + "@typescript-eslint/parser": "6.14.0", "@vitest/coverage-v8": "0.34.6", - "@vue/runtime-core": "3.3.8", + "@vue/runtime-core": "3.3.12", "acorn": "8.11.2", "cross-env": "7.0.3", - "cypress": "13.5.1", - "eslint": "8.53.0", - "eslint-plugin-import": "2.29.0", - "eslint-plugin-vue": "9.18.1", + "cypress": "13.6.1", + "eslint": "8.56.0", + "eslint-plugin-import": "2.29.1", + "eslint-plugin-vue": "9.19.2", "fast-glob": "3.3.2", "happy-dom": "10.0.3", + "intersection-observer": "0.12.2", "micromatch": "4.0.5", "msw": "1.3.2", "msw-storybook-addon": "1.10.0", - "nodemon": "3.0.1", - "prettier": "3.1.0", + "nodemon": "3.0.2", + "prettier": "3.1.1", "react": "18.2.0", "react-dom": "18.2.0", "start-server-and-test": "2.0.3", - "storybook": "7.5.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.22" + "vue-tsc": "1.8.25" } } diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts new file mode 100644 index 0000000000..d419ade527 --- /dev/null +++ b/packages/frontend/src/_dev_boot_.ts @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// devモードで起動される際(index.htmlを使うとき)はrouterが暴発してしまってうまく読み込めない。 +// よって、devモードとして起動されるときはビルド時に組み込む形としておく。 +// (pnpm start時はpugファイルの中で静的リソースとして読み込むようになっており、この問題は起こっていない) +import '@phosphor-icons/web/bold'; + +await main(); + +import('@/_boot_.js'); + +/** + * backend/src/server/web/boot.jsで差し込まれている起動処理のうち、最低限必要なものを模倣するための処理 + */ +async function main() { + const forceError = localStorage.getItem('forceError'); + if (forceError != null) { + renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); + } + + //#region Detect language & fetch translations + + // dev-modeの場合は常に取り直す + const supportedLangs = _LANGS_.map(it => it[0]); + let lang: string | null | undefined = localStorage.getItem('lang'); + if (lang == null || !supportedLangs.includes(lang)) { + if (supportedLangs.includes(navigator.language)) { + lang = navigator.language; + } else { + lang = supportedLangs.find(x => x.split('-')[0] === navigator.language); + + // Fallback + if (lang == null) lang = 'en-US'; + } + } + + // TODO:今のままだと言語ファイル変更後はpnpm devをリスタートする必要があるので、chokidarを使ったり等で対応できるようにする + const locale = _LANGS_FULL_.find(it => it[0] === lang); + localStorage.setItem('lang', lang); + localStorage.setItem('locale', JSON.stringify(locale[1])); + localStorage.setItem('localeVersion', _VERSION_); + //#endregion + + //#region Theme + const theme = localStorage.getItem('theme'); + if (theme) { + for (const [k, v] of Object.entries(JSON.parse(theme))) { + document.documentElement.style.setProperty(`--${k}`, v.toString()); + + // HTMLの theme-color 適用 + if (k === 'htmlThemeColor') { + for (const tag of document.head.children) { + if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { + tag.setAttribute('content', v); + break; + } + } + } + } + } + const colorScheme = localStorage.getItem('colorScheme'); + if (colorScheme) { + document.documentElement.style.setProperty('color-scheme', colorScheme); + } + //#endregion + + const fontSize = localStorage.getItem('fontSize'); + if (fontSize) { + document.documentElement.classList.add('f-' + fontSize); + } + + const useSystemFont = localStorage.getItem('useSystemFont'); + if (useSystemFont) { + document.documentElement.classList.add('useSystemFont'); + } + + const wallpaper = localStorage.getItem('wallpaper'); + if (wallpaper) { + document.documentElement.style.backgroundImage = `url(${wallpaper})`; + } + + const customCss = localStorage.getItem('customCss'); + if (customCss && customCss.length > 0) { + const style = document.createElement('style'); + style.innerHTML = customCss; + document.head.appendChild(style); + } +} + +function renderError(code: string, details?: string) { + console.log(code, details); +} diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index f19cad331b..05008194f0 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -16,7 +16,7 @@ import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js'; // TODO: 他のタブと永続化されたstateを同期 -type Account = Misskey.entities.MeDetailed; +type Account = Misskey.entities.MeDetailed & { token: string }; const accountData = miLocalStorage.getItem('account'); @@ -284,7 +284,7 @@ export async function openAccountMenu(opts: { text: i18n.ts.profile, to: `/@${ $i.username }`, avatar: $i, - }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { + }, { type: 'divider' }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { type: 'parent' as const, icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.addAccount, diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 82bb44c071..63f169d9ad 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -3,28 +3,25 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue'; +import { computed, watch, version as vueVersion, App } from 'vue'; import { compareVersions } from 'compare-versions'; import widgets from '@/widgets/index.js'; import directives from '@/directives/index.js'; import components from '@/components/index.js'; -import { version, ui, lang, updateLocale } from '@/config.js'; +import { version, lang, updateLocale, locale } from '@/config.js'; import { applyTheme } from '@/scripts/theme.js'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js'; -import { i18n, updateI18n } from '@/i18n.js'; -import { confirm, alert, post, popup, toast } from '@/os.js'; -import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js'; +import { updateI18n } from '@/i18n.js'; +import { $i, refreshAccount, login } from '@/account.js'; import { defaultStore, ColdDeviceStorage } from '@/store.js'; import { fetchInstance, instance } from '@/instance.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { reloadChannel } from '@/scripts/unison-reload.js'; -import { reactionPicker } from '@/scripts/reaction-picker.js'; import { getUrlWithoutLoginId } from '@/scripts/login-id.js'; 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 { mainRouter } from '@/router.js'; export async function common(createVue: () => App<Element>) { console.info(`Sharkey v${version}`); @@ -88,7 +85,7 @@ export async function common(createVue: () => App<Element>) { //#region Detect language & fetch translations const localeVersion = miLocalStorage.getItem('localeVersion'); - const localeOutdated = (localeVersion == null || localeVersion !== version); + const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null); if (localeOutdated) { const res = await window.fetch(`/assets/locales/${lang}.${version}.json`); if (res.status === 200) { @@ -187,6 +184,12 @@ export async function common(createVue: () => App<Element>) { if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme)); if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme)); defaultStore.set('themeInitial', false); + } else { + if (defaultStore.state.darkMode) { + applyTheme(darkTheme.value); + } else { + applyTheme(lightTheme.value); + } } }); @@ -202,24 +205,28 @@ export async function common(createVue: () => App<Element>) { } }, { immediate: true }); - if (defaultStore.state.keepScreenOn) { - if ('wakeLock' in navigator) { + // Keep screen on + const onVisibilityChange = () => document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { try { navigator.wakeLock.request('screen'); } catch (err) { return; } - - document.addEventListener('visibilitychange', async () => { - if (document.visibilityState === 'visible') { - try { - navigator.wakeLock.request('screen'); - } catch (err) { - return; - } - } - }); } + }); + if (defaultStore.state.keepScreenOn && 'wakeLock' in navigator) { + navigator.wakeLock.request('screen') + .then(onVisibilityChange) + .catch(() => { + // On WebKit-based browsers, user activation is required to send wake lock request + // https://webkit.org/blog/13862/the-user-activation-api/ + document.addEventListener( + 'click', + () => navigator.wakeLock.request('screen').then(onVisibilityChange), + { once: true }, + ); + }); } //#region Fetch user diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 72ae8d64a3..cdc1d11ca2 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -3,14 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; +import { createApp, markRaw, defineAsyncComponent } from 'vue'; import { common } from './common.js'; -import { version, ui, lang, updateLocale } from '@/config.js'; -import { i18n, updateI18n } from '@/i18n.js'; +import { ui } from '@/config.js'; +import { i18n } from '@/i18n.js'; import { confirm, alert, post, popup, toast } from '@/os.js'; import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; -import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js'; +import { $i, updateAccount, signout } from '@/account.js'; import { defaultStore, ColdDeviceStorage } from '@/store.js'; import { makeHotkey } from '@/scripts/hotkey.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; @@ -19,6 +19,7 @@ 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'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( @@ -30,6 +31,7 @@ export async function mainBoot() { )); reactionPicker.init(); + emojiPicker.init(); if (isClientUpdated && $i) { popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed'); @@ -71,6 +73,14 @@ export async function mainBoot() { }, }; + if (defaultStore.state.enableSeasonalScreenEffect) { + const month = new Date().getMonth() + 1; + if (month === 12 || month === 1) { + const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect; + new SnowfallEffect().render(); + } + } + if ($i) { // only add post shortcuts if logged in hotkeys['p|n'] = post; diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts index 9b4670e130..92ee074afb 100644 --- a/packages/frontend/src/boot/sub-boot.ts +++ b/packages/frontend/src/boot/sub-boot.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; +import { createApp, defineAsyncComponent } from 'vue'; import { common } from './common.js'; export async function subBoot() { diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index 14247f4bf5..611c8a1782 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :text="report.comment"/> </div> <hr/> - <div>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></div> + <div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link">@{{ report.reporter.username }}</MkA></div> <div v-if="report.assignee"> {{ i18n.ts.moderator }}: <MkAcct :user="report.assignee"/> @@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; @@ -56,11 +57,11 @@ const emit = defineEmits<{ (ev: 'resolved', reportId: string): void; }>(); -let forward = $ref(props.report.forwarded); +const forward = ref(props.report.forwarded); function resolve() { os.apiWithDialog('admin/resolve-abuse-user-report', { - forward: forward, + forward: forward.value, reportId: props.report.id, }).then(() => { emit('resolved', props.report.id); diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue index aac7f508a1..40f9ad4057 100644 --- a/packages/frontend/src/components/MkAchievements.vue +++ b/packages/frontend/src/components/MkAchievements.vue @@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { onMounted } from 'vue'; +import { onMounted, ref, computed } from 'vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js'; @@ -67,15 +67,15 @@ const props = withDefaults(defineProps<{ withDescription: true, }); -let achievements = $ref(); -const lockedAchievements = $computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements ?? []).some(a => a.name === x))); +const achievements = ref(); +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 => { - achievements = []; + achievements.value = []; for (const t of ACHIEVEMENT_TYPES) { const a = res.find(x => x.name === t); - if (a) achievements.push(a); + if (a) achievements.value.push(a); } //achievements = res.sort((a, b) => b.unlockedAt - a.unlockedAt); }); diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue index cd2c4d8264..0e252f7b1d 100644 --- a/packages/frontend/src/components/MkAnalogClock.vue +++ b/packages/frontend/src/components/MkAnalogClock.vue @@ -138,45 +138,45 @@ const texts = computed(() => { }); let enabled = true; -let majorGraduationColor = $ref<string>(); +const majorGraduationColor = ref<string>(); //let minorGraduationColor = $ref<string>(); -let sHandColor = $ref<string>(); -let mHandColor = $ref<string>(); -let hHandColor = $ref<string>(); -let nowColor = $ref<string>(); -let h = $ref<number>(0); -let m = $ref<number>(0); -let s = $ref<number>(0); -let hAngle = $ref<number>(0); -let mAngle = $ref<number>(0); -let sAngle = $ref<number>(0); -let disableSAnimate = $ref(false); +const sHandColor = ref<string>(); +const mHandColor = ref<string>(); +const hHandColor = ref<string>(); +const nowColor = ref<string>(); +const h = ref<number>(0); +const m = ref<number>(0); +const s = ref<number>(0); +const hAngle = ref<number>(0); +const mAngle = ref<number>(0); +const sAngle = ref<number>(0); +const disableSAnimate = ref(false); let sOneRound = false; const sLine = ref<SVGPathElement>(); function tick() { const now = props.now(); now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset); - const previousS = s; - const previousM = m; - const previousH = h; - s = now.getSeconds(); - m = now.getMinutes(); - h = now.getHours(); - if (previousS === s && previousM === m && previousH === h) { + const previousS = s.value; + const previousM = m.value; + const previousH = h.value; + s.value = now.getSeconds(); + m.value = now.getMinutes(); + h.value = now.getHours(); + if (previousS === s.value && previousM === m.value && previousH === h.value) { return; } - hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6); - mAngle = Math.PI * (m + s / 60) / 30; + hAngle.value = Math.PI * (h.value % (props.twentyfour ? 24 : 12) + (m.value + s.value / 60) / 60) / (props.twentyfour ? 12 : 6); + mAngle.value = Math.PI * (m.value + s.value / 60) / 30; if (sOneRound && sLine.value) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない) - sAngle = Math.PI * 60 / 30; + sAngle.value = Math.PI * 60 / 30; defaultIdlingRenderScheduler.delete(tick); sLine.value.addEventListener('transitionend', () => { - disableSAnimate = true; + disableSAnimate.value = true; requestAnimationFrame(() => { - sAngle = 0; + sAngle.value = 0; requestAnimationFrame(() => { - disableSAnimate = false; + disableSAnimate.value = false; if (enabled) { defaultIdlingRenderScheduler.add(tick); } @@ -184,9 +184,9 @@ function tick() { }); }, { once: true }); } else { - sAngle = Math.PI * s / 30; + sAngle.value = Math.PI * s.value / 30; } - sOneRound = s === 59; + sOneRound = s.value === 59; } tick(); @@ -195,12 +195,12 @@ function calcColors() { const computedStyle = getComputedStyle(document.documentElement); const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark(); const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(); - majorGraduationColor = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'; + majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'; //minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; - sHandColor = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'; - mHandColor = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString(); - hHandColor = accent; - nowColor = accent; + sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'; + mHandColor.value = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString(); + hHandColor.value = accent; + nowColor.value = accent; } calcColors(); diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue index 70d101a9d3..284ee8f3f8 100644 --- a/packages/frontend/src/components/MkAnimBg.vue +++ b/packages/frontend/src/components/MkAnimBg.vue @@ -21,8 +21,9 @@ const props = withDefaults(defineProps<{ focus: 1.0, }); -function loadShader(gl, type, source) { +function loadShader(gl: WebGLRenderingContext, type: number, source: string) { const shader = gl.createShader(type); + if (shader == null) return null; gl.shaderSource(shader, source); gl.compileShader(shader); @@ -38,11 +39,13 @@ function loadShader(gl, type, source) { return shader; } -function initShaderProgram(gl, vsSource, fsSource) { +function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) { const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); const shaderProgram = gl.createProgram(); + if (shaderProgram == null || vertexShader == null || fragmentShader == null) return null; + gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); @@ -63,8 +66,10 @@ let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null; onMounted(() => { const canvas = canvasEl.value!; - canvas.width = canvas.offsetWidth; - canvas.height = canvas.offsetHeight; + let width = canvas.offsetWidth; + let height = canvas.offsetHeight; + canvas.width = width; + canvas.height = height; const gl = canvas.getContext('webgl', { premultipliedAlpha: true }); if (gl == null) return; @@ -197,6 +202,7 @@ onMounted(() => { gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) ); } `); + if (shaderProgram == null) return; gl.useProgram(shaderProgram); const u_resolution = gl.getUniformLocation(shaderProgram, 'u_resolution'); @@ -226,7 +232,23 @@ onMounted(() => { gl!.uniform1f(u_time, 0); gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4); } else { - function render(timeStamp) { + function render(timeStamp: number) { + let sizeChanged = false; + if (Math.abs(height - canvas.offsetHeight) > 2) { + height = canvas.offsetHeight; + canvas.height = height; + sizeChanged = true; + } + if (Math.abs(width - canvas.offsetWidth) > 2) { + width = canvas.offsetWidth; + canvas.width = width; + sizeChanged = true; + } + if (sizeChanged && gl) { + gl.uniform2fv(u_resolution, [width, height]); + gl.viewport(0, 0, width, height); + } + gl!.uniform1f(u_time, timeStamp); gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4); diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index 9596ce6077..60978eb0bd 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -43,6 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only fixed :instant="true" :initialText="c.form.text" + :initialCw="c.form.cw" /> </div> <MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened"> @@ -60,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { Ref } from 'vue'; +import { Ref, ref } from 'vue'; import * as os from '@/os.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -87,16 +88,17 @@ function g(id) { return props.components.find(x => x.value.id === id).value; } -let valueForSwitch = $ref(c.default ?? false); +const valueForSwitch = ref(c.default ?? false); function onSwitchUpdate(v) { - valueForSwitch = v; + valueForSwitch.value = v; if (c.onChange) c.onChange(v); } function openPostForm() { os.post({ initialText: c.form.text, + initialCw: c.form.cw, instant: true, }); } diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 9e92c4bb03..1f819cf601 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -242,29 +242,7 @@ function exec() { return; } - const matched: EmojiDef[] = []; - const max = 30; - - emojiDb.value.some(x => { - if (x.name.toLowerCase().startsWith(props.q ? props.q.toLowerCase() : '') && !x.aliasOf && !matched.some(y => y.emoji.toLowerCase() === x.emoji.toLowerCase())) matched.push(x); - return matched.length === max; - }); - - if (matched.length < max) { - emojiDb.value.some(x => { - if (x.name.toLowerCase().startsWith(props.q ? props.q.toLowerCase() : '') && !matched.some(y => y.emoji.toLowerCase() === x.emoji.toLowerCase())) matched.push(x); - return matched.length === max; - }); - } - - if (matched.length < max) { - emojiDb.value.some(x => { - if (x.name.toLowerCase().includes(props.q ? props.q.toLowerCase() : '') && !matched.some(y => y.emoji.toLowerCase() === x.emoji.toLowerCase())) matched.push(x); - return matched.length === max; - }); - } - - emojis.value = matched; + emojis.value = emojiAutoComplete(props.q.toLowerCase(), emojiDb.value); } else if (props.type === 'mfmTag') { if (!props.q || props.q === '') { mfmTags.value = MFM_TAGS; @@ -275,6 +253,78 @@ function exec() { } } +type EmojiScore = { emoji: EmojiDef, score: number }; + +function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] { + if (!query) { + return []; + } + + const matched = new Map<string, EmojiScore>(); + + // 前方一致(エイリアスなし) + emojiDb.some(x => { + if (x.name.toLowerCase().startsWith(query) && !x.aliasOf) { + matched.set(x.name, { emoji: x, score: query.length + 1 }); + } + return matched.size === max; + }); + + // 前方一致(エイリアス込み) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.toLowerCase().startsWith(query) && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length }); + } + return matched.size === max; + }); + } + + // 部分一致(エイリアス込み) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.toLowerCase().includes(query) && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 }); + } + return matched.size === max; + }); + } + + // 簡易あいまい検索(3文字以上) + if (matched.size < max && query.length > 3) { + const queryChars = [...query]; + const hitEmojis = new Map<string, EmojiScore>(); + + for (const x of emojiDb) { + // 文字列の位置を進めながら、クエリの文字を順番に探す + + let pos = 0; + let hit = 0; + for (const c of queryChars) { + pos = x.name.toLowerCase().indexOf(c, pos); + if (pos <= -1) break; + hit++; + } + + // 半分以上の文字が含まれていればヒットとする + if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) { + hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 }); + } + } + + // ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分) + [...hitEmojis.values()] + .sort((x, y) => y.score - x.score) + .slice(0, 6) + .forEach(it => matched.set(it.emoji.name, it)); + } + + return [...matched.values()] + .sort((x, y) => y.score - x.score) + .slice(0, max) + .map(it => it.emoji); +} + function onMousedown(event: Event) { if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close(); } @@ -309,12 +359,25 @@ function onKeydown(event: KeyboardEvent) { } break; - case 'Tab': case 'ArrowDown': cancel(); selectNext(); break; + case 'Tab': + if (event.shiftKey) { + if (select.value !== -1) { + cancel(); + selectPrev(); + } else { + props.close(); + } + } else { + cancel(); + selectNext(); + } + break; + default: event.stopPropagation(); props.textarea.focus(); diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index 2fdc2bbe07..9fcc49d3f0 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, onMounted } from 'vue'; +import { nextTick, onMounted, shallowRef } from 'vue'; const props = defineProps<{ type?: 'button' | 'submit' | 'reset'; @@ -59,13 +59,13 @@ const emit = defineEmits<{ (ev: 'click', payload: MouseEvent): void; }>(); -let el = $shallowRef<HTMLElement | null>(null); -let ripples = $shallowRef<HTMLElement | null>(null); +const el = shallowRef<HTMLElement | null>(null); +const ripples = shallowRef<HTMLElement | null>(null); onMounted(() => { if (props.autofocus) { nextTick(() => { - el!.focus(); + el.value!.focus(); }); } }); @@ -88,11 +88,11 @@ function onMousedown(evt: MouseEvent): void { const rect = target.getBoundingClientRect(); const ripple = document.createElement('div'); - ripple.classList.add(ripples!.dataset.childrenClass!); + ripple.classList.add(ripples.value!.dataset.childrenClass!); ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px'; ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px'; - ripples!.appendChild(ripple); + ripples.value!.appendChild(ripple); const circleCenterX = evt.clientX - rect.left; const circleCenterY = evt.clientY - rect.top; @@ -107,7 +107,7 @@ function onMousedown(evt: MouseEvent): void { ripple.style.opacity = '0'; }, 1000); window.setTimeout(() => { - if (ripples) ripples.removeChild(ripple); + if (ripples.value) ripples.value.removeChild(ripple); }, 2000); } </script> diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue index 9c6e2f00bd..96590a469b 100644 --- a/packages/frontend/src/components/MkChannelPreview.vue +++ b/packages/frontend/src/components/MkChannelPreview.vue @@ -4,49 +4,70 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1"> - <div class="banner" :style="bannerStyle"> - <div class="fade"></div> - <div class="name"><i class="ph-television ph-bold ph-lg"></i> {{ channel.name }}</div> - <div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div> - <div class="status"> - <div> - <i class="ph-users ph-bold ph-lg"></i> - <I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"> - <template #n> - <b>{{ channel.usersCount }}</b> - </template> - </I18n> - </div> - <div> - <i class="ph-pencil ph-bold ph-lg"></i> - <I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"> - <template #n> - <b>{{ channel.notesCount }}</b> - </template> - </I18n> +<div style="position: relative;"> + <MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1" @click="updateLastReadedAt"> + <div class="banner" :style="bannerStyle"> + <div class="fade"></div> + <div class="name"><i class="ph-television ph-bold ph-lg"></i> {{ channel.name }}</div> + <div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div> + <div class="status"> + <div> + <i class="ph-users ph-bold ph-lg"></i> + <I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"> + <template #n> + <b>{{ channel.usersCount }}</b> + </template> + </I18n> + </div> + <div> + <i class="ph-pencil ph-bold ph-lg"></i> + <I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"> + <template #n> + <b>{{ channel.notesCount }}</b> + </template> + </I18n> + </div> </div> </div> - </div> - <article v-if="channel.description"> - <p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p> - </article> - <footer> - <span v-if="channel.lastNotedAt"> - {{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/> - </span> - </footer> -</MkA> + <article v-if="channel.description"> + <p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p> + </article> + <footer> + <span v-if="channel.lastNotedAt"> + {{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/> + </span> + </footer> + </MkA> + <div + v-if="channel.lastNotedAt && (channel.isFavorited || channel.isFollowing) && (!lastReadedAt || Date.parse(channel.lastNotedAt) > lastReadedAt)" + class="indicator" + ></div> +</div> </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref, watch } from 'vue'; import { i18n } from '@/i18n.js'; +import { miLocalStorage } from '@/local-storage.js'; const props = defineProps<{ channel: Record<string, any>; }>(); +const getLastReadedAt = (): number | null => { + return miLocalStorage.getItemAsJson(`channelLastReadedAt:${props.channel.id}`) ?? null; +}; + +const lastReadedAt = ref(getLastReadedAt()); + +watch(() => props.channel.id, () => { + lastReadedAt.value = getLastReadedAt(); +}); + +const updateLastReadedAt = () => { + lastReadedAt.value = props.channel.lastNotedAt ? Date.parse(props.channel.lastNotedAt) : Date.now(); +}; + const bannerStyle = computed(() => { if (props.channel.bannerUrl) { return { backgroundImage: `url(${props.channel.bannerUrl})` }; @@ -170,4 +191,17 @@ const bannerStyle = computed(() => { } } +.indicator { + position: absolute; + top: 0; + right: 0; + transform: translate(25%, -25%); + background-color: var(--accent); + border: solid var(--bg) 4px; + border-radius: 100%; + width: 1.5rem; + height: 1.5rem; + aspect-ratio: 1 / 1; +} + </style> diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index fe7077bdbf..adb3c134ae 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -74,7 +74,7 @@ const props = defineProps({ }, }); -let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); +const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>(); const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); const negate = arr => arr.map(x => -x); @@ -268,7 +268,7 @@ const render = () => { gradient, }, }, - plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl)] : [])], + plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl.value)] : [])], }); }; diff --git a/packages/frontend/src/components/MkChartLegend.vue b/packages/frontend/src/components/MkChartLegend.vue index 546bc0b4b1..c265fe6e97 100644 --- a/packages/frontend/src/components/MkChartLegend.vue +++ b/packages/frontend/src/components/MkChartLegend.vue @@ -13,29 +13,30 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { shallowRef } from 'vue'; import { Chart, LegendItem } from 'chart.js'; const props = defineProps({ }); -let chart = $shallowRef<Chart>(); -let items = $shallowRef<LegendItem[]>([]); +const chart = shallowRef<Chart>(); +const items = shallowRef<LegendItem[]>([]); function update(_chart: Chart, _items: LegendItem[]) { - chart = _chart, - items = _items; + chart.value = _chart, + items.value = _items; } function onClick(item: LegendItem) { - if (chart == null) return; - const { type } = chart.config; + if (chart.value == null) return; + const { type } = chart.value.config; if (type === 'pie' || type === 'doughnut') { // Pie and doughnut charts only have a single dataset and visibility is per item - chart.toggleDataVisibility(item.index); + chart.value.toggleDataVisibility(item.index); } else { - chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex)); + chart.value.setDatasetVisibility(item.datasetIndex, !chart.value.isDatasetVisible(item.datasetIndex)); } - chart.update(); + chart.value.update(); } defineExpose({ diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 71914c6886..1e72319010 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted, onUnmounted } from 'vue'; +import { computed, onMounted, onUnmounted, ref } from 'vue'; import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; import * as os from '@/os.js'; import { useInterval } from '@/scripts/use-interval.js'; @@ -29,8 +29,8 @@ import { claimAchievement } from '@/scripts/achievements.js'; const saveData = game.saveData; const cookies = computed(() => saveData.value?.cookies); -let cps = $ref(0); -let prevCookies = $ref(0); +const cps = ref(0); +const prevCookies = ref(0); function onClick(ev: MouseEvent) { const x = ev.clientX; @@ -48,9 +48,9 @@ function onClick(ev: MouseEvent) { } useInterval(() => { - const diff = saveData.value!.cookies - prevCookies; - cps = diff; - prevCookies = saveData.value!.cookies; + const diff = saveData.value!.cookies - prevCookies.value; + cps.value = diff; + prevCookies.value = saveData.value!.cookies; }, 1000, { immediate: false, afterMounted: true, @@ -63,7 +63,7 @@ useInterval(game.save, 1000 * 5, { onMounted(async () => { await game.load(); - prevCookies = saveData.value!.cookies; + prevCookies.value = saveData.value!.cookies; }); onUnmounted(() => { diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue index 21684b462a..19418cd4da 100644 --- a/packages/frontend/src/components/MkCode.core.vue +++ b/packages/frontend/src/components/MkCode.core.vue @@ -54,7 +54,7 @@ watch(() => props.lang, (to) => { return new Promise((resolve) => { fetchLanguage(to).then(() => resolve); }); -}, { immediate: true, }); +}, { immediate: true }); </script> <style scoped lang="scss"> @@ -62,7 +62,7 @@ watch(() => props.lang, (to) => { padding: 1em; margin: .5em 0; overflow: auto; - border-radius: .3em; + border-radius: 8px; & pre, & code { diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index b39e6ff23c..2c016e4d7c 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -4,18 +4,27 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> - <Suspense> - <template #fallback> - <MkLoading v-if="!inline ?? true" /> - </template> - <code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code> - <XCode v-else :code="code" :lang="lang"/> - </Suspense> +<Suspense> + <template #fallback> + <MkLoading v-if="!inline ?? true"/> + </template> + <code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code> + <XCode v-else-if="show && lang" :code="code" :lang="lang"/> + <pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre> + <button v-else :class="$style.codePlaceholderRoot" @click="show = true"> + <div :class="$style.codePlaceholderContainer"> + <div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div> + <div>{{ i18n.ts.clickToShow }}</div> + </div> + </button> +</Suspense> </template> <script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; +import { defineAsyncComponent, ref } from 'vue'; import MkLoading from '@/components/global/MkLoading.vue'; +import { defaultStore } from '@/store.js'; +import { i18n } from '@/i18n.js'; defineProps<{ code: string; @@ -23,6 +32,8 @@ defineProps<{ inline?: boolean; }>(); +const show = ref(!defaultStore.state.dataSaver.code); + const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue')); </script> @@ -36,4 +47,42 @@ const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue')) padding: .1em; border-radius: .3em; } + +.codeBlockFallbackRoot { + display: block; + overflow-wrap: anywhere; + color: #D4D4D4; + background: #1E1E1E; + padding: 1em; + margin: .5em 0; + overflow: auto; + border-radius: 8px; +} + +.codeBlockFallbackCode { + font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; +} + +.codePlaceholderRoot { + display: block; + width: 100%; + background: none; + border: none; + outline: none; + font: inherit; + color: inherit; + cursor: pointer; + + box-sizing: border-box; + border-radius: 8px; + padding: 24px; + margin-top: 4px; + color: #D4D4D4; + background: #1E1E1E; +} + +.codePlaceholderContainer { + text-align: center; + font-size: 0.8em; +} </style> diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index 5434042684..c9bcc71196 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -4,30 +4,38 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[$style.codeEditorRoot, { [$style.disabled]: disabled, [$style.focused]: focused }]"> - <div :class="$style.codeEditorScroller"> - <textarea - ref="inputEl" - v-model="vModel" - :class="[$style.textarea]" - :disabled="disabled" - :required="required" - :readonly="readonly" - autocomplete="off" - wrap="off" - spellcheck="false" - @focus="focused = true" - @blur="focused = false" - @keydown="onKeydown($event)" - @input="onInput" - ></textarea> - <XCode :class="$style.codeEditorHighlighter" :codeEditor="true" :code="v" :lang="lang"/> +<div> + <div :class="$style.label" @click="focus"><slot name="label"></slot></div> + <div :class="[$style.codeEditorRoot, { [$style.focused]: focused }]"> + <div :class="$style.codeEditorScroller"> + <textarea + ref="inputEl" + v-model="vModel" + :class="[$style.textarea]" + :disabled="disabled" + :required="required" + :readonly="readonly" + autocomplete="off" + wrap="off" + spellcheck="false" + @focus="focused = true" + @blur="focused = false" + @keydown="onKeydown($event)" + @input="onInput" + ></textarea> + <XCode :class="$style.codeEditorHighlighter" :codeEditor="true" :code="v" :lang="lang"/> + </div> </div> + <div :class="$style.caption"><slot name="caption"></slot></div> + <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </template> <script lang="ts" setup> import { ref, watch, toRefs, shallowRef, nextTick } from 'vue'; +import { debounce } from 'throttle-debounce'; +import MkButton from '@/components/MkButton.vue'; +import { i18n } from '@/i18n.js'; import XCode from '@/components/MkCode.core.vue'; const props = withDefaults(defineProps<{ @@ -36,6 +44,8 @@ const props = withDefaults(defineProps<{ required?: boolean; readonly?: boolean; disabled?: boolean; + debounce?: boolean; + manualSave?: boolean; }>(), { lang: 'js', }); @@ -54,6 +64,8 @@ const focused = ref(false); const changed = ref(false); const inputEl = shallowRef<HTMLTextAreaElement>(); +const focus = () => inputEl.value?.focus(); + const onInput = (ev) => { v.value = ev.target?.value ?? v.value; changed.value = true; @@ -100,16 +112,48 @@ const updated = () => { emit('update:modelValue', v.value); }; +const debouncedUpdated = debounce(1000, updated); + watch(modelValue, newValue => { v.value = newValue ?? ''; }); -watch(v, () => { - updated(); +watch(v, newValue => { + if (!props.manualSave) { + if (props.debounce) { + debouncedUpdated(); + } else { + updated(); + } + } }); </script> <style lang="scss" module> +.label { + font-size: 0.85em; + padding: 0 0 8px 0; + user-select: none; + + &:empty { + display: none; + } +} + +.caption { + font-size: 0.85em; + padding: 8px 0 0 0; + color: var(--fgTransparentWeak); + + &:empty { + display: none; + } +} + +.save { + margin: 8px 0 0 0; +} + .codeEditorRoot { min-width: 100%; max-width: 100%; @@ -117,6 +161,7 @@ watch(v, () => { overflow-y: hidden; box-sizing: border-box; margin: 0; + border-radius: 6px; padding: 0; color: var(--fg); border: solid 1px var(--panel); @@ -139,6 +184,10 @@ watch(v, () => { height: 100%; } +.textarea, .codeEditorHighlighter { + margin: 0; +} + .textarea { position: absolute; top: 0; @@ -153,7 +202,10 @@ watch(v, () => { caret-color: rgb(225, 228, 232); background-color: transparent; border: 0; + border-radius: 6px; outline: 0; + min-width: calc(100% - 24px); + height: 100%; padding: 12px; line-height: 1.5em; font-size: 1em; diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue index 79b1949640..4f15e88951 100644 --- a/packages/frontend/src/components/MkColorInput.vue +++ b/packages/frontend/src/components/MkColorInput.vue @@ -24,8 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; -import { i18n } from '@/i18n.js'; +import { ref, shallowRef, toRefs } from 'vue'; const props = defineProps<{ modelValue: string | null; diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue index 6cca7fc353..b78252be89 100644 --- a/packages/frontend/src/components/MkContextMenu.vue +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onBeforeUnmount } from 'vue'; +import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue'; import MkMenu from './MkMenu.vue'; import { MenuItem } from './types/menu.vue'; import contains from '@/scripts/contains.js'; @@ -34,9 +34,9 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -let rootEl = $shallowRef<HTMLDivElement>(); +const rootEl = shallowRef<HTMLDivElement>(); -let zIndex = $ref<number>(os.claimZIndex('high')); +const zIndex = ref<number>(os.claimZIndex('high')); const SCROLLBAR_THICKNESS = 16; @@ -44,8 +44,8 @@ onMounted(() => { let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 - const width = rootEl.offsetWidth; - const height = rootEl.offsetHeight; + const width = rootEl.value.offsetWidth; + const height = rootEl.value.offsetHeight; if (left + width - window.pageXOffset >= (window.innerWidth - SCROLLBAR_THICKNESS)) { left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset; @@ -63,8 +63,8 @@ onMounted(() => { left = 0; } - rootEl.style.top = `${top}px`; - rootEl.style.left = `${left}px`; + rootEl.value.style.top = `${top}px`; + rootEl.value.style.left = `${left}px`; document.body.addEventListener('mousedown', onMousedown); }); @@ -74,7 +74,7 @@ onBeforeUnmount(() => { }); function onMousedown(evt: Event) { - if (!contains(rootEl, evt.target) && (rootEl !== evt.target)) emit('closed'); + if (!contains(rootEl.value, evt.target) && (rootEl.value !== evt.target)) emit('closed'); } </script> diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 81f3936600..0a1ddd3171 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, shallowRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import Cropper from 'cropperjs'; import tinycolor from 'tinycolor2'; @@ -56,10 +56,10 @@ const props = defineProps<{ }>(); const imgUrl = getProxiedImageUrl(props.file.url, undefined, true); -let dialogEl = $shallowRef<InstanceType<typeof MkModalWindow>>(); -let imgEl = $shallowRef<HTMLImageElement>(); +const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>(); +const imgEl = shallowRef<HTMLImageElement>(); let cropper: Cropper | null = null; -let loading = $ref(true); +const loading = ref(true); const ok = async () => { const promise = new Promise<Misskey.entities.DriveFile>(async (res) => { @@ -94,16 +94,16 @@ const ok = async () => { const f = await promise; emit('ok', f); - dialogEl!.close(); + dialogEl.value!.close(); }; const cancel = () => { emit('cancel'); - dialogEl!.close(); + dialogEl.value!.close(); }; const onImageLoad = () => { - loading = false; + loading.value = false; if (cropper) { cropper.getCropperImage()!.$center('contain'); @@ -112,7 +112,7 @@ const onImageLoad = () => { }; onMounted(() => { - cropper = new Cropper(imgEl!, { + cropper = new Cropper(imgEl.value!, { }); const computedStyle = getComputedStyle(document.documentElement); diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue index 0cdaf7c9bd..4a6d2dfba2 100644 --- a/packages/frontend/src/components/MkCwButton.vue +++ b/packages/frontend/src/components/MkCwButton.vue @@ -16,7 +16,23 @@ import MkButton from '@/components/MkButton.vue'; const props = defineProps<{ modelValue: boolean; - note: Misskey.entities.Note; + text: string | null; + renote: Misskey.entities.Note | null; + files: Misskey.entities.DriveFile[]; + poll?: { + expiresAt: string | null; + multiple: boolean; + choices: { + isVoted: boolean; + text: string; + votes: number; + }[]; + } | { + choices: string[]; + multiple: boolean; + expiresAt: string | null; + expiredAfter: string | null; + }; }>(); const emit = defineEmits<{ @@ -25,9 +41,10 @@ const emit = defineEmits<{ const label = computed(() => { return concat([ - props.note.text ? [i18n.t('_cw.chars', { count: props.note.text.length })] : [], - props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length })] : [], - props.note.poll != null ? [i18n.ts.poll] : [], + props.text ? [i18n.t('_cw.chars', { count: props.text.length })] : [], + props.renote ? [i18n.ts.quote] : [], + props.files.length !== 0 ? [i18n.t('_cw.files', { count: props.files.length })] : [], + props.poll != null ? [i18n.ts.poll] : [], ] as string[][]).join(' / '); }); diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index e0692eb383..2c0f6a4d78 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown"> <template v-if="input.type === 'password'" #prefix><i class="ph-lock ph-bold ph-lg"></i></template> <template #caption> - <span v-if="okButtonDisabled && disabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/> - <span v-else-if="okButtonDisabled && disabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/> + <span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/> + <span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/> </template> </MkInput> <MkSelect v-if="select" v-model="selectedValue" autofocus> @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </MkSelect> <div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons"> - <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabled" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton> + <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton> <MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton> </div> <div v-if="actions" :class="$style.buttons"> @@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'; +import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue'; import MkModal from '@/components/MkModal.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -122,24 +122,21 @@ const modal = shallowRef<InstanceType<typeof MkModal>>(); const inputValue = ref<string | number | null>(props.input?.default ?? null); const selectedValue = ref(props.select?.default ?? null); -let disabledReason = $ref<null | 'charactersExceeded' | 'charactersBelow'>(null); -const okButtonDisabled = $computed<boolean>(() => { +const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => { if (props.input) { if (props.input.minLength) { if ((inputValue.value || inputValue.value === '') && (inputValue.value as string).length < props.input.minLength) { - disabledReason = 'charactersBelow'; - return true; + return 'charactersBelow'; } } if (props.input.maxLength) { if (inputValue.value && (inputValue.value as string).length > props.input.maxLength) { - disabledReason = 'charactersExceeded'; - return true; + return 'charactersExceeded'; } } } - return false; + return null; }); function done(canceled: boolean, result?) { diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 9f79a44d4c..dcaaa72cf4 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -39,6 +39,7 @@ import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { claimAchievement } from '@/scripts/achievements.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { MenuItem } from '@/types/menu.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -250,7 +251,7 @@ function setAsUploadFolder() { } function onContextmenu(ev: MouseEvent) { - let menu; + let menu: MenuItem[]; menu = [{ text: i18n.ts.openInWindow, icon: 'ph-app-window ph-bold ph-lg', @@ -260,18 +261,18 @@ function onContextmenu(ev: MouseEvent) { }, { }, 'closed'); }, - }, null, { + }, { type: 'divider' }, { text: i18n.ts.rename, icon: 'ph-textbox ph-bold ph-lg', action: rename, - }, null, { + }, { type: 'divider' }, { text: i18n.ts.delete, icon: 'ph-trash ph-bold ph-lg', danger: true, action: deleteFolder, }]; if (defaultStore.state.devMode) { - menu = menu.concat([null, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ph-identification-card ph-bold ph-lg', text: i18n.ts.copyFolderId, action: () => { diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 5281541927..00bb0e6e2b 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -616,7 +616,7 @@ function getMenu() { type: 'switch', text: i18n.ts.keepOriginalUploading, ref: keepOriginal, - }, null, { + }, { type: 'divider' }, { text: i18n.ts.addFile, type: 'label', }, { @@ -627,7 +627,7 @@ function getMenu() { text: i18n.ts.fromUrl, icon: 'ph-link ph-bold ph-lg', action: () => { urlUpload(); }, - }, null, { + }, { type: 'divider' }, { text: folder.value ? folder.value.name : i18n.ts.drive, type: 'label', }, folder.value ? { diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 00e0a0e042..49c146b68d 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと --> -<section> +<!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) --> +<section v-if="!hasChildSection" v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);"> <header class="_acrylic" @click="shown = !shown"> - <i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> ({{ emojis.length }}) + <i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> (<i class="ph-bold ph-lg"></i>:{{ emojis.length }}) </header> <div v-if="shown" class="body"> <button @@ -23,15 +24,52 @@ SPDX-License-Identifier: AGPL-3.0-only </button> </div> </section> +<!-- フォルダの中にはカスタム絵文字やフォルダがある --> +<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);"> + <header class="_acrylic" @click="shown = !shown"> + <i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> (<i class="ph-folder ph-bold ph-lg"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }}) + </header> + <div v-if="shown" style="padding-left: 9px;"> + <MkEmojiPickerSection + v-for="child in customEmojiTree" + :key="`custom:${child.value}`" + :initialShown="initialShown" + :emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))" + :hasChildSection="child.children.length !== 0" + :customEmojiTree="child.children" + @chosen="nestedChosen" + > + {{ child.value || i18n.ts.other }} + </MkEmojiPickerSection> + </div> + <div v-if="shown" class="body"> + <button + v-for="emoji in emojis" + :key="emoji" + :data-emoji="emoji" + class="_button item" + @pointerenter="computeButtonTitle" + @click="emit('chosen', emoji, $event)" + > + <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/> + <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/> + </button> + </div> +</section> </template> <script lang="ts" setup> import { ref, computed, Ref } from 'vue'; -import { getEmojiName } from '@/scripts/emojilist.js'; +import { i18n } from '../i18n.js'; +import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js'; +import { customEmojis } from '@/custom-emojis.js'; +import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue'; const props = defineProps<{ emojis: string[] | Ref<string[]>; initialShown?: boolean; + hasChildSection?: boolean; + customEmojiTree?: CustomEmojiFolderTree[]; }>(); const emit = defineEmits<{ @@ -49,4 +87,7 @@ function computeButtonTitle(ev: MouseEvent): void { elm.title = getEmojiName(emoji) ?? emoji; } +function nestedChosen(emoji: any, ev?: MouseEvent) { + emit('chosen', emoji, ev); +} </script> diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 50ed8048bb..b7e329d7c2 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only </section> <div v-if="tab === 'index'" class="group index"> - <section v-if="showPinned"> + <section v-if="showPinned && pinned.length > 0"> <div class="body"> <button v-for="emoji in pinned" @@ -73,18 +73,20 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-once class="group"> <header class="_acrylic">{{ i18n.ts.customEmojis }}</header> <XSection - v-for="category in customEmojiCategories" - :key="`custom:${category}`" + v-for="child in customEmojiFolderRoot.children" + :key="`custom:${child.value}`" :initialShown="false" - :emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).filter(filterAvailable).map(e => `:${e.name}:`))" + :emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value).filter(filterAvailable).map(e => `:${e.name}:`))" + :hasChildSection="child.children.length !== 0" + :customEmojiTree="child.children" @chosen="chosen" > - {{ category || i18n.ts.other }} + {{ child.value || i18n.ts.other }} </XSection> </div> <div v-once class="group"> <header class="_acrylic">{{ i18n.ts.emoji }}</header> - <XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" @chosen="chosen">{{ category }}</XSection> + <XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" :hasChildSection="false" @chosen="chosen">{{ category }}</XSection> </div> </div> <div class="tabs"> @@ -100,7 +102,14 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, shallowRef, computed, watch, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import XSection from '@/components/MkEmojiPicker.section.vue'; -import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories, getEmojiName } from '@/scripts/emojilist.js'; +import { + emojilist, + emojiCharByCategory, + UnicodeEmojiDef, + unicodeEmojiCategories as categories, + getEmojiName, + CustomEmojiFolderTree, +} from '@/scripts/emojilist.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import * as os from '@/os.js'; import { isTouchUsing } from '@/scripts/touch.js'; @@ -112,10 +121,11 @@ import { $i } from '@/account.js'; const props = withDefaults(defineProps<{ showPinned?: boolean; - asReactionPicker?: boolean; + pinnedEmojis?: string[]; maxHeight?: number; asDrawer?: boolean; asWindow?: boolean; + asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう }>(), { showPinned: true, }); @@ -128,22 +138,50 @@ const searchEl = shallowRef<HTMLInputElement>(); const emojisEl = shallowRef<HTMLDivElement>(); const { - reactions: pinned, - reactionPickerSize, - reactionPickerWidth, - reactionPickerHeight, - disableShowingAnimatedImages, + emojiPickerScale, + emojiPickerWidth, + emojiPickerHeight, recentlyUsedEmojis, } = defaultStore.reactiveState; -const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1); -const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3); -const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2); +const pinned = computed(() => props.pinnedEmojis); +const size = computed(() => emojiPickerScale.value); +const width = computed(() => emojiPickerWidth.value); +const height = computed(() => emojiPickerHeight.value); const q = ref<string>(''); -const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]); +const searchResultCustom = ref<Misskey.entities.EmojiSimple[]>([]); const searchResultUnicode = ref<UnicodeEmojiDef[]>([]); const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index'); +const customEmojiFolderRoot: CustomEmojiFolderTree = { value: '', category: '', children: [] }; + +function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree { + const parts = input.split('/').map(p => p.trim()); + let currentNode: CustomEmojiFolderTree = root; + + for (const part of parts) { + let existingNode = currentNode.children.find((node) => node.value === part); + + if (!existingNode) { + const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] }; + currentNode.children.push(newNode); + existingNode = newNode; + } + + currentNode = existingNode; + } + + return currentNode; +} + +customEmojiCategories.value.forEach(ec => { + if (ec !== null) { + parseAndMergeCategories(ec, customEmojiFolderRoot); + } +}); + +parseAndMergeCategories('', customEmojiFolderRoot); + watch(q, () => { if (emojisEl.value) emojisEl.value.scrollTop = 0; @@ -158,7 +196,7 @@ watch(q, () => { const searchCustom = () => { const max = 100; const emojis = customEmojis.value; - const matches = new Set<Misskey.entities.CustomEmoji>(); + const matches = new Set<Misskey.entities.EmojiSimple>(); const exactMatch = emojis.find(emoji => emoji.name === newQ); if (exactMatch) matches.add(exactMatch); @@ -288,7 +326,7 @@ watch(q, () => { searchResultUnicode.value = Array.from(searchUnicode()); }); -function filterAvailable(emoji: Misskey.entities.CustomEmoji): boolean { +function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean { return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))); } @@ -305,7 +343,7 @@ function reset() { q.value = ''; } -function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string { +function getKey(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef): string { return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`; } @@ -329,7 +367,7 @@ function chosen(emoji: any, ev?: MouseEvent) { emit('chosen', key); // 最近使った絵文字更新 - if (!pinned.value.includes(key)) { + if (!pinned.value?.includes(key)) { let recents = defaultStore.state.recentlyUsedEmojis; recents = recents.filter((emoji: any) => emoji !== key); recents.unshift(key); @@ -572,8 +610,7 @@ defineExpose({ position: sticky; top: 0; left: 0; - height: 32px; - line-height: 32px; + line-height: 28px; z-index: 1; padding: 0 8px; font-size: 12px; diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 581d815d66..4068a79f08 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="modal" v-slot="{ type, maxHeight }" :zPriority="'middle'" - :preferType="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" + :preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'" :transparentBg="true" :manualShowing="manualShowing" :src="src" @@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only class="_popup _shadow" :class="{ [$style.drawer]: type === 'drawer' }" :showPinned="showPinned" + :pinnedEmojis="pinnedEmojis" :asReactionPicker="asReactionPicker" :asDrawer="type === 'drawer'" :max-height="maxHeight" @@ -36,15 +37,19 @@ import MkModal from '@/components/MkModal.vue'; import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; import { defaultStore } from '@/store.js'; -withDefaults(defineProps<{ +const props = withDefaults(defineProps<{ manualShowing?: boolean | null; src?: HTMLElement; showPinned?: boolean; + pinnedEmojis?: string[], asReactionPicker?: boolean; + choseAndClose?: boolean; }>(), { manualShowing: null, showPinned: true, + pinnedEmojis: undefined, asReactionPicker: false, + choseAndClose: true, }); const emit = defineEmits<{ @@ -58,7 +63,9 @@ const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>(); function chosen(emoji: any) { emit('done', emoji); - modal.value?.close(); + if (props.choseAndClose) { + modal.value?.close(); + } } function opening() { diff --git a/packages/frontend/src/components/MkFeaturedPhotos.vue b/packages/frontend/src/components/MkFeaturedPhotos.vue index cef1943d5c..6d1bad7433 100644 --- a/packages/frontend/src/components/MkFeaturedPhotos.vue +++ b/packages/frontend/src/components/MkFeaturedPhotos.vue @@ -12,7 +12,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; -const meta = ref<Misskey.entities.DetailedInstanceMetadata>(); +const meta = ref<Misskey.entities.MetaResponse>(); os.api('meta', { detail: true }).then(gotMeta => { meta.value = gotMeta; diff --git a/packages/frontend/src/components/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue index b582b88712..b799fb9447 100644 --- a/packages/frontend/src/components/MkFileCaptionEditWindow.vue +++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { shallowRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -42,12 +42,12 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); -let caption = $ref(props.default); +const caption = ref(props.default); async function ok() { - emit('done', caption); - dialog.close(); + emit('done', caption.value); + dialog.value.close(); } </script> diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 30e93ef9e4..03621a4255 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, onMounted } from 'vue'; +import { nextTick, onMounted, shallowRef, ref } from 'vue'; import { defaultStore } from '@/store.js'; const props = withDefaults(defineProps<{ @@ -70,10 +70,10 @@ const getBgColor = (el: HTMLElement) => { } }; -let rootEl = $shallowRef<HTMLElement>(); -let bgSame = $ref(false); -let opened = $ref(props.defaultOpen); -let openedAtLeastOnce = $ref(props.defaultOpen); +const rootEl = shallowRef<HTMLElement>(); +const bgSame = ref(false); +const opened = ref(props.defaultOpen); +const openedAtLeastOnce = ref(props.defaultOpen); function enter(el) { const elementHeight = el.getBoundingClientRect().height; @@ -98,20 +98,20 @@ function afterLeave(el) { } function toggle() { - if (!opened) { - openedAtLeastOnce = true; + if (!opened.value) { + openedAtLeastOnce.value = true; } nextTick(() => { - opened = !opened; + opened.value = !opened.value; }); } onMounted(() => { const computedStyle = getComputedStyle(document.documentElement); - const parentBg = getBgColor(rootEl.parentElement); + const parentBg = getBgColor(rootEl.value.parentElement); const myBg = computedStyle.getPropertyValue('--panel'); - bgSame = parentBg === myBg; + bgSame.value = parentBg === myBg; }); </script> diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index eebb753db1..d1b1956a03 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onBeforeUnmount, onMounted } from 'vue'; +import { onBeforeUnmount, onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { useStream } from '@/stream.js'; @@ -57,9 +57,9 @@ const emit = defineEmits<{ (_: 'update:user', value: Misskey.entities.UserDetailed): void }>(); -let isFollowing = $ref(props.user.isFollowing); -let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou); -let wait = $ref(false); +const isFollowing = ref(props.user.isFollowing); +const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou); +const wait = ref(false); const connection = useStream().useChannel('main'); if (props.user.isFollowing == null) { @@ -71,16 +71,16 @@ if (props.user.isFollowing == null) { function onFollowChange(user: Misskey.entities.UserDetailed) { if (user.id === props.user.id) { - isFollowing = user.isFollowing; - hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; + isFollowing.value = user.isFollowing; + hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou; } } async function onClick() { - wait = true; + wait.value = true; try { - if (isFollowing) { + if (isFollowing.value) { const { canceled } = await os.confirm({ type: 'warning', text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }), @@ -92,11 +92,11 @@ async function onClick() { userId: props.user.id, }); } else { - if (hasPendingFollowRequestFromYou) { + if (hasPendingFollowRequestFromYou.value) { await os.api('following/requests/cancel', { userId: props.user.id, }); - hasPendingFollowRequestFromYou = false; + hasPendingFollowRequestFromYou.value = false; } else { await os.api('following/create', { userId: props.user.id, @@ -104,9 +104,9 @@ async function onClick() { }); emit('update:user', { ...props.user, - withReplies: defaultStore.state.defaultWithReplies + withReplies: defaultStore.state.defaultWithReplies, }); - hasPendingFollowRequestFromYou = true; + hasPendingFollowRequestFromYou.value = true; claimAchievement('following1'); @@ -127,7 +127,7 @@ async function onClick() { } catch (err) { console.error(err); } finally { - wait = false; + wait.value = false; } } diff --git a/packages/frontend/src/components/MkForgotPassword.vue b/packages/frontend/src/components/MkForgotPassword.vue index 521ac11d12..9b57688a02 100644 --- a/packages/frontend/src/components/MkForgotPassword.vue +++ b/packages/frontend/src/components/MkForgotPassword.vue @@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -53,19 +53,19 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -let dialog: InstanceType<typeof MkModalWindow> = $ref(); +const dialog = ref<InstanceType<typeof MkModalWindow>>(); -let username = $ref(''); -let email = $ref(''); -let processing = $ref(false); +const username = ref(''); +const email = ref(''); +const processing = ref(false); async function onSubmit() { - processing = true; + processing.value = true; await os.apiWithDialog('request-reset-password', { - username, - email, + username: username.value, + email: email.value, }); emit('done'); - dialog.close(); + dialog.value.close(); } </script> diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index 24404728ca..6f882cfab7 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -26,11 +26,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template> <template v-if="form[item].description" #caption>{{ form[item].description }}</template> </MkInput> - <MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text"> + <MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" :mfmAutocomplete="form[item].treatAsMfm"> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template> <template v-if="form[item].description" #caption>{{ form[item].description }}</template> </MkInput> - <MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]"> + <MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" :mfmAutocomplete="form[item].treatAsMfm" :mfmPreview="form[item].treatAsMfm"> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template> <template v-if="form[item].description" #caption>{{ form[item].description }}</template> </MkTextarea> diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue index 185a49b5a9..c0b20507fc 100644 --- a/packages/frontend/src/components/MkGoogle.vue +++ b/packages/frontend/src/components/MkGoogle.vue @@ -23,7 +23,7 @@ const query = ref(props.q); const search = () => { const sp = new URLSearchParams(); sp.append('q', query.value); - window.open(`https://www.google.com/search?${sp.toString()}`, '_blank'); + window.open(`https://www.google.com/search?${sp.toString()}`, '_blank', 'noopener'); }; </script> diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue index 0022531e58..a57e6c9292 100644 --- a/packages/frontend/src/components/MkHeatmap.vue +++ b/packages/frontend/src/components/MkHeatmap.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, nextTick, watch } from 'vue'; +import { onMounted, nextTick, watch, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; @@ -27,11 +27,11 @@ const props = defineProps<{ src: string; }>(); -const rootEl = $shallowRef<HTMLDivElement>(null); -const chartEl = $shallowRef<HTMLCanvasElement>(null); +const rootEl = shallowRef<HTMLDivElement>(null); +const chartEl = shallowRef<HTMLCanvasElement>(null); const now = new Date(); let chartInstance: Chart = null; -let fetching = $ref(true); +const fetching = ref(true); const { handler: externalTooltipHandler } = useChartTooltip({ position: 'middle', @@ -42,8 +42,8 @@ async function renderChart() { chartInstance.destroy(); } - const wide = rootEl.offsetWidth > 700; - const narrow = rootEl.offsetWidth < 400; + const wide = rootEl.value.offsetWidth > 700; + const narrow = rootEl.value.offsetWidth < 400; const weeks = wide ? 50 : narrow ? 10 : 25; const chartLimit = 7 * weeks; @@ -88,7 +88,7 @@ async function renderChart() { values = raw.deliverFailed; } - fetching = false; + fetching.value = false; await nextTick(); @@ -101,7 +101,7 @@ async function renderChart() { const marginEachCell = 4; - chartInstance = new Chart(chartEl, { + chartInstance = new Chart(chartEl.value, { type: 'matrix', data: { datasets: [{ @@ -210,7 +210,7 @@ async function renderChart() { } watch(() => props.src, () => { - fetching = true; + fetching.value = true; renderChart(); }); diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index 4fb573fdbc..942861e1f4 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -21,7 +21,6 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts"> -import { $ref } from 'vue/macros'; import DrawBlurhash from '@/workers/draw-blurhash?worker'; import TestWebGL2 from '@/workers/test-webgl2?worker'; import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js'; @@ -58,7 +57,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol </script> <script lang="ts" setup> -import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue'; +import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue'; import { v4 as uuid } from 'uuid'; import { render } from 'buraha'; import { defaultStore } from '@/store.js'; @@ -98,41 +97,41 @@ const viewId = uuid(); const canvas = shallowRef<HTMLCanvasElement>(); const root = shallowRef<HTMLDivElement>(); const img = shallowRef<HTMLImageElement>(); -let loaded = $ref(false); -let canvasWidth = $ref(64); -let canvasHeight = $ref(64); -let imgWidth = $ref(props.width); -let imgHeight = $ref(props.height); -let bitmapTmp = $ref<CanvasImageSource | undefined>(); -const hide = computed(() => !loaded || props.forceBlurhash); +const loaded = ref(false); +const canvasWidth = ref(64); +const canvasHeight = ref(64); +const imgWidth = ref(props.width); +const imgHeight = ref(props.height); +const bitmapTmp = ref<CanvasImageSource | undefined>(); +const hide = computed(() => !loaded.value || props.forceBlurhash); function waitForDecode() { if (props.src != null && props.src !== '') { nextTick() .then(() => img.value?.decode()) .then(() => { - loaded = true; + loaded.value = true; }, error => { console.log('Error occurred during decoding image', img.value, error); }); } else { - loaded = false; + loaded.value = false; } } watch([() => props.width, () => props.height, root], () => { const ratio = props.width / props.height; if (ratio > 1) { - canvasWidth = Math.round(64 * ratio); - canvasHeight = 64; + canvasWidth.value = Math.round(64 * ratio); + canvasHeight.value = 64; } else { - canvasWidth = 64; - canvasHeight = Math.round(64 / ratio); + canvasWidth.value = 64; + canvasHeight.value = Math.round(64 / ratio); } const clientWidth = root.value?.clientWidth ?? 300; - imgWidth = clientWidth; - imgHeight = Math.round(clientWidth / ratio); + imgWidth.value = clientWidth; + imgHeight.value = Math.round(clientWidth / ratio); }, { immediate: true, }); @@ -140,15 +139,15 @@ watch([() => props.width, () => props.height, root], () => { function drawImage(bitmap: CanvasImageSource) { // canvasがない(mountedされていない)場合はTmpに保存しておく if (!canvas.value) { - bitmapTmp = bitmap; + bitmapTmp.value = bitmap; return; } // canvasがあれば描画する - bitmapTmp = undefined; + bitmapTmp.value = undefined; const ctx = canvas.value.getContext('2d'); if (!ctx) return; - ctx.drawImage(bitmap, 0, 0, canvasWidth, canvasHeight); + ctx.drawImage(bitmap, 0, 0, canvasWidth.value, canvasHeight.value); } function drawAvg() { @@ -160,7 +159,7 @@ function drawAvg() { // avgColorでお茶をにごす ctx.beginPath(); ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888'; - ctx.fillRect(0, 0, canvasWidth, canvasHeight); + ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value); } async function draw() { @@ -212,8 +211,8 @@ watch(() => props.hash, () => { onMounted(() => { // drawImageがmountedより先に呼ばれている場合はここで描画する - if (bitmapTmp) { - drawImage(bitmapTmp); + if (bitmapTmp.value) { + drawImage(bitmapTmp.value); } waitForDecode(); }); diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index 6f237761a8..b4b4e1b0b7 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -43,11 +43,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; +import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; import { debounce } from 'throttle-debounce'; import MkButton from '@/components/MkButton.vue'; import { useInterval } from '@/scripts/use-interval.js'; import { i18n } from '@/i18n.js'; +import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js'; const props = defineProps<{ modelValue: string | number | null; @@ -59,6 +60,7 @@ const props = defineProps<{ placeholder?: string; autofocus?: boolean; autocomplete?: string; + mfmAutocomplete?: boolean | SuggestionType[], autocapitalize?: string; spellcheck?: boolean; step?: any; @@ -93,6 +95,7 @@ const height = props.small ? 33 : props.large ? 39 : 36; +let autocomplete: Autocomplete; const focus = () => inputEl.value.focus(); const onInput = (ev: KeyboardEvent) => { @@ -160,6 +163,16 @@ onMounted(() => { focus(); } }); + + if (props.mfmAutocomplete) { + autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete); + } +}); + +onUnmounted(() => { + if (autocomplete) { + autocomplete.detach(); + } }); defineExpose({ diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue index 6af9c6ccb5..9cde197e19 100644 --- a/packages/frontend/src/components/MkInstanceCardMini.vue +++ b/packages/frontend/src/components/MkInstanceCardMini.vue @@ -15,21 +15,22 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkMiniChart from '@/components/MkMiniChart.vue'; import * as os from '@/os.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const props = defineProps<{ - instance: Misskey.entities.Instance; + instance: Misskey.entities.FederationInstance; }>(); -let chartValues = $ref<number[] | null>(null); +const chartValues = ref<number[] | null>(null); os.apiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => { // 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く - res.requests.received.splice(0, 1); - chartValues = res.requests.received; + res['requests.received'].splice(0, 1); + chartValues.value = res['requests.received']; }); function getInstanceIcon(instance): string { diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue index 509254de74..7b763ad385 100644 --- a/packages/frontend/src/components/MkInstanceStats.vue +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref, shallowRef } from 'vue'; import { Chart } from 'chart.js'; import MkSelect from '@/components/MkSelect.vue'; import MkChart from '@/components/MkChart.vue'; @@ -100,11 +100,11 @@ import { initChart } from '@/scripts/init-chart.js'; initChart(); const chartLimit = 500; -let chartSpan = $ref<'hour' | 'day'>('hour'); -let chartSrc = $ref('active-users'); -let heatmapSrc = $ref('active-users'); -let subDoughnutEl = $shallowRef<HTMLCanvasElement>(); -let pubDoughnutEl = $shallowRef<HTMLCanvasElement>(); +const chartSpan = ref<'hour' | 'day'>('hour'); +const chartSrc = ref('active-users'); +const heatmapSrc = ref('active-users'); +const subDoughnutEl = shallowRef<HTMLCanvasElement>(); +const pubDoughnutEl = shallowRef<HTMLCanvasElement>(); const { handler: externalTooltipHandler1 } = useChartTooltip({ position: 'middle', @@ -163,7 +163,7 @@ function createDoughnut(chartEl, tooltip, data) { onMounted(() => { os.apiGet('federation/stats', { limit: 30 }).then(fedStats => { - createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({ + createDoughnut(subDoughnutEl.value, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount, @@ -172,7 +172,7 @@ onMounted(() => { }, })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }])); - createDoughnut(pubDoughnutEl, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({ + createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount, diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index f0650e48f1..e358a1c549 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { computed } from 'vue'; import { instanceName } from '@/config.js'; import { instance as Instance } from '@/instance.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; @@ -30,7 +30,7 @@ const instance = props.instance ?? { themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content, }; -const faviconUrl = $computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico'); +const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico'); const themeColor = instance.themeColor ?? '#777777'; diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue index 8e3561e2b8..54d997d1c9 100644 --- a/packages/frontend/src/components/MkInviteCode.vue +++ b/packages/frontend/src/components/MkInviteCode.vue @@ -67,7 +67,7 @@ import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; const props = defineProps<{ - invite: Misskey.entities.Invite; + invite: Misskey.entities.InviteCode; moderator?: boolean; }>(); diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue index 17f8af4f63..099082f539 100644 --- a/packages/frontend/src/components/MkLaunchPad.vue +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { shallowRef } from 'vue'; import MkModal from '@/components/MkModal.vue'; import { navbarItemDef } from '@/navbar.js'; import { defaultStore } from '@/store.js'; @@ -48,7 +48,7 @@ const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'pop deviceKind === 'smartphone' ? 'drawer' : 'dialog'; -const modal = $shallowRef<InstanceType<typeof MkModal>>(); +const modal = shallowRef<InstanceType<typeof MkModal>>(); const menu = defaultStore.state.menu; @@ -63,7 +63,7 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => })); function close() { - modal.close(); + modal.value.close(); } </script> @@ -101,6 +101,8 @@ function close() { vertical-align: bottom; height: 100px; border-radius: var(--radius); + padding: 10px; + box-sizing: border-box; &:hover { color: var(--accent); diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index 114b9b4faf..e16307c762 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <component - :is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel" :target="target" + :is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target" :title="url" > <slot></slot> @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; +import { defineAsyncComponent, ref } from 'vue'; import { url as local } from '@/config.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import * as os from '@/os.js'; @@ -29,13 +29,13 @@ const self = props.url.startsWith(local); const attr = self ? 'to' : 'href'; const target = self ? null : '_blank'; -const el = $ref(); +const el = ref(); -useTooltip($$(el), (showing) => { +useTooltip(el, (showing) => { os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { showing, url: props.url, - source: el, + source: el.value, }, {}, 'closed'); }); </script> diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 42a709ae26..4594c8a1db 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, shallowRef, watch } from 'vue'; +import { shallowRef, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; @@ -42,7 +42,7 @@ const props = withDefaults(defineProps<{ }); const audioEl = shallowRef<HTMLAudioElement>(); -let hide = $ref(true); +const hide = ref(true); watch(audioEl, () => { if (audioEl.value) { diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 1fa42c1e48..0040f00dc2 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <ImgWithBlurhash :hash="image.blurhash" - :src="(defaultStore.state.enableDataSaverMode && hide) ? null : url" + :src="(defaultStore.state.dataSaver.media && hide) ? null : url" :forceBlurhash="hide" :cover="hide || cover" :alt="image.comment || image.name" @@ -32,8 +32,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="hide"> <div :class="$style.hiddenText"> <div :class="$style.hiddenTextWrapper"> - <b v-if="image.isSensitive" style="display: block;"><i class="ph-eye-closed ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> - <b v-else style="display: block;"><i class="ph-image-square ph-bold ph-lg"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b> + <b v-if="image.isSensitive" style="display: block;"><i class="ph-eye-closed ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ph-image-square ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b> <span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span> </div> </div> @@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import bytes from '@/filters/bytes.js'; @@ -74,10 +74,10 @@ const props = withDefaults(defineProps<{ controls: true, }); -let hide = $ref(true); -let darkMode: boolean = $ref(defaultStore.state.darkMode); +const hide = ref(true); +const darkMode = ref<boolean>(defaultStore.state.darkMode); -const url = $computed(() => (props.raw || defaultStore.state.loadRawImages) +const url = computed(() => (props.raw || defaultStore.state.loadRawImages) ? props.image.url : defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(props.image.url) @@ -88,14 +88,14 @@ function onclick() { if (!props.controls) { return; } - if (hide) { - hide = false; + if (hide.value) { + hide.value = false; } } // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする watch(() => props.image, () => { - hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore'); + hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore'); }, { deep: true, immediate: true, @@ -106,7 +106,7 @@ function showMenu(ev: MouseEvent) { text: i18n.ts.hide, icon: 'ph-eye-slash ph-bold ph-lg', action: () => { - hide = true; + hide.value = true; }, }, ...(iAmModerator ? [{ text: i18n.ts.markAsSensitive, diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 610978e4ab..46e32ef2d8 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div ref="root" :class="$style.root"> +<div :class="$style.root"> <XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/> <div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container"> <div @@ -28,43 +28,8 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> -<script lang="ts"> -/** - * アスペクト比算出のためにHTMLElement.clientWidthを使うが、 - * 大変重たいのでコンテナ要素とメディアリスト幅のペアをキャッシュする - * (タイムラインごとにスクロールコンテナが存在する前提だが……) - */ -const widthCache = new Map<Element, number>(); - -/** - * コンテナ要素がリサイズされたらキャッシュを削除する - */ -const ro = new ResizeObserver(entries => { - for (const entry of entries) { - widthCache.delete(entry.target); - } -}); - -async function getClientWidthWithCache(targetEl: HTMLElement, containerEl: HTMLElement, count = 0) { - if (_DEV_) console.log('getClientWidthWithCache', { targetEl, containerEl, count, cache: widthCache.get(containerEl) }); - if (widthCache.has(containerEl)) return widthCache.get(containerEl)!; - - const width = targetEl.clientWidth; - - if (count <= 10 && width < 64) { - // widthが64未満はおかしいのでリトライする - await new Promise(resolve => setTimeout(resolve, 50)); - return getClientWidthWithCache(targetEl, containerEl, count + 1); - } - - widthCache.set(containerEl, width); - ro.observe(containerEl); - return width; -} -</script> - <script lang="ts" setup> -import { onMounted, onUnmounted, shallowRef } from 'vue'; +import { computed, onMounted, onUnmounted, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import PhotoSwipeLightbox from 'photoswipe/lightbox'; import PhotoSwipe from 'photoswipe'; @@ -76,19 +41,16 @@ import XModPlayer from '@/components/MkModPlayer.vue'; import * as os from '@/os.js'; import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@/const.js'; import { defaultStore } from '@/store.js'; -import { getScrollContainer, getBodyScrollHeight } from '@/scripts/scroll.js'; const props = defineProps<{ mediaList: Misskey.entities.DriveFile[]; raw?: boolean; }>(); -const root = shallowRef<HTMLDivElement>(); -const container = shallowRef<HTMLElement | null | undefined>(undefined); const gallery = shallowRef<HTMLDivElement>(); const pswpZIndex = os.claimZIndex('middle'); document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString()); -const count = $computed(() => props.mediaList.filter(media => previewable(media)).length); +const count = computed(() => props.mediaList.filter(media => previewable(media)).length); let lightbox: PhotoSwipeLightbox | null; const popstateHandler = (): void => { @@ -97,12 +59,8 @@ const popstateHandler = (): void => { } }; -/** - * アスペクト比をmediaListWithOneImageAppearanceに基づいていい感じに調整する - * aspect-ratioではなくheightを使う - */ async function calcAspectRatio() { - if (!gallery.value || !root.value) return; + if (!gallery.value) return; let img = props.mediaList[0]; @@ -111,41 +69,22 @@ async function calcAspectRatio() { return; } - if (!container.value) container.value = getScrollContainer(root.value); - const width = container.value ? await getClientWidthWithCache(root.value, container.value) : root.value.clientWidth; - - const heightMin = (ratio: number) => { - const imgResizeRatio = width / img.properties.width; - const imgDrawHeight = img.properties.height * imgResizeRatio; - const maxHeight = width * ratio; - const height = Math.min(imgDrawHeight, maxHeight); - if (_DEV_) console.log('Image height calculated:', { width, properties: img.properties, imgResizeRatio, imgDrawHeight, maxHeight, height }); - return `${height}px`; - }; + const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`; switch (defaultStore.state.mediaListWithOneImageAppearance) { case '16_9': - gallery.value.style.height = heightMin(9 / 16); + gallery.value.style.aspectRatio = ratioMax(16 / 9); break; case '1_1': - gallery.value.style.height = heightMin(1); + gallery.value.style.aspectRatio = ratioMax(1 / 1); break; case '2_3': - gallery.value.style.height = heightMin(3 / 2); + gallery.value.style.aspectRatio = ratioMax(2 / 3); break; - default: { - const maxHeight = Math.max(64, (container.value ? container.value.clientHeight : getBodyScrollHeight()) * 0.5 || 360); - if (width === 0 || !maxHeight) return; - const imgResizeRatio = width / img.properties.width; - const imgDrawHeight = img.properties.height * imgResizeRatio; - gallery.value.style.height = `${Math.max(64, Math.min(imgDrawHeight, maxHeight))}px`; - gallery.value.style.minHeight = 'initial'; - gallery.value.style.maxHeight = 'initial'; + default: + gallery.value.style.aspectRatio = ''; break; - } } - - gallery.value.style.aspectRatio = 'initial'; } const isModule = (file: Misskey.entities.DriveFile): boolean => { diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 33a9b0fbf9..4f8560f0f1 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -7,8 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="hide" :class="[$style.hidden, (video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitiveContainer]" @click="hide = false"> <!-- 【注意】dataSaverMode が有効になっている際には、hide が false になるまでサムネイルや動画を読み込まないようにすること --> <div :class="$style.sensitive"> - <b v-if="video.isSensitive" style="display: block;"><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> - <b v-else style="display: block;"><i class="ph-film-strip ph-bold ph-lg"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b> + <b v-if="video.isSensitive" style="display: block;"><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ph-film-strip ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b> <span>{{ i18n.ts.clickToShow }}</span> </div> </div> @@ -37,18 +37,25 @@ import * as Misskey from 'misskey-js'; import bytes from '@/filters/bytes.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; +import hasAudio from '@/scripts/media-has-audio.js'; const props = defineProps<{ video: Misskey.entities.DriveFile; }>(); -const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore')); +const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore')); const videoEl = shallowRef<HTMLVideoElement>(); watch(videoEl, () => { if (videoEl.value) { videoEl.value.volume = 0.3; + hasAudio(videoEl.value).then(had => { + if (!had) { + videoEl.value.loop = videoEl.value.muted = true; + videoEl.value.play(); + } + }); } }); </script> diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index 83f56dc1a2..b0f997a1b9 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -13,9 +13,9 @@ SPDX-License-Identifier: AGPL-3.0-only @contextmenu.self="e => e.preventDefault()" > <template v-for="(item, i) in items2"> - <div v-if="item === null" role="separator" :class="$style.divider"></div> + <div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div> <span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]"> - <span>{{ item.text }}</span> + <span style="opacity: 0.7;">{{ item.text }}</span> </span> <span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]"> <span><MkEllipsis/></span> @@ -23,32 +23,44 @@ SPDX-License-Identifier: AGPL-3.0-only <MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> - <span>{{ item.text }}</span> - <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + <div :class="$style.item_content"> + <span :class="$style.item_content_text">{{ item.text }}</span> + <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + </div> </MkA> <a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> - <span>{{ item.text }}</span> - <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + <div :class="$style.item_content"> + <span :class="$style.item_content_text">{{ item.text }}</span> + <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + </div> </a> <button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/> - <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + <div v-if="item.indicate" :class="$style.item_content"> + <span :class="$style.indicator"><i class="_indicatorCircle"></i></span> + </div> </button> <button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> - <span :class="$style.switchText">{{ item.text }}</span> + <div :class="$style.item_content"> + <span :class="[$style.item_content_text, $style.switchText]">{{ item.text }}</span> + </div> </button> <button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> - <span style="pointer-events: none;">{{ item.text }}</span> - <span :class="$style.caret" style="pointer-events: none;"><i class="ph-caret-right ph-bold ph-lg ti-fw"></i></span> + <div :class="$style.item_content"> + <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span> + <span :class="$style.caret" style="pointer-events: none;"><i class="ph-caret-right ph-bold ph-lg ti-fw"></i></span> + </div> </button> <button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> - <span>{{ item.text }}</span> - <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + <div :class="$style.item_content"> + <span :class="$style.item_content_text">{{ item.text }}</span> + <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + </div> </button> </template> <span v-if="items2.length === 0" :class="[$style.none, $style.item]"> @@ -62,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts"> -import { Ref, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; +import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'; import { focusPrev, focusNext } from '@/scripts/focus.js'; import MkSwitchButton from '@/components/MkSwitch.button.vue'; import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu'; @@ -90,19 +102,19 @@ const emit = defineEmits<{ (ev: 'hide'): void; }>(); -let itemsEl = $shallowRef<HTMLDivElement>(); +const itemsEl = shallowRef<HTMLDivElement>(); -let items2: InnerMenuItem[] = $ref([]); +const items2 = ref<InnerMenuItem[]>([]); -let child = $shallowRef<InstanceType<typeof XChild>>(); +const child = shallowRef<InstanceType<typeof XChild>>(); -let keymap = $computed(() => ({ +const keymap = computed(() => ({ 'up|k|shift+tab': focusUp, 'down|j|tab': focusDown, 'esc': close, })); -let childShowingItem = $ref<MenuItem | null>(); +const childShowingItem = ref<MenuItem | null>(); let preferClick = isTouchUsing || props.asDrawer; @@ -115,22 +127,22 @@ watch(() => props.items, () => { if (item && 'then' in item) { // if item is Promise items[i] = { type: 'pending' }; item.then(actualItem => { - items2[i] = actualItem; + items2.value[i] = actualItem; }); } } - items2 = items as InnerMenuItem[]; + items2.value = items as InnerMenuItem[]; }, { immediate: true, }); const childMenu = ref<MenuItem[] | null>(); -let childTarget = $shallowRef<HTMLElement | null>(); +const childTarget = shallowRef<HTMLElement | null>(); function closeChild() { childMenu.value = null; - childShowingItem = null; + childShowingItem.value = null; } function childActioned() { @@ -139,8 +151,8 @@ function childActioned() { } const onGlobalMousedown = (event: MouseEvent) => { - if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return; - if (child && child.checkHit(event)) return; + if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target))) return; + if (child.value && child.value.checkHit(event)) return; closeChild(); }; @@ -177,10 +189,10 @@ async function showChildren(item: MenuParent, ev: MouseEvent) { }); emit('hide'); } else { - childTarget = ev.currentTarget ?? ev.target; + childTarget.value = ev.currentTarget ?? ev.target; // これでもリアクティビティは保たれる childMenu.value = children; - childShowingItem = item; + childShowingItem.value = item; } } @@ -202,14 +214,14 @@ function focusDown() { } function switchItem(item: MenuSwitch & { ref: any }) { - if (item.disabled) return; + if (item.disabled !== undefined && (typeof item.disabled === 'boolean' ? item.disabled : item.disabled.value)) return; item.ref = !item.ref; } onMounted(() => { if (props.viaKeyboard) { nextTick(() => { - if (itemsEl) focusNext(itemsEl.children[0], true, false); + if (itemsEl.value) focusNext(itemsEl.value.children[0], true, false); }); } @@ -228,6 +240,7 @@ onBeforeUnmount(() => { .root { padding: 8px 0; box-sizing: border-box; + max-width: 100vw; min-width: 200px; overflow: auto; overscroll-behavior: contain; @@ -267,7 +280,8 @@ onBeforeUnmount(() => { } .item { - display: block; + display: flex; + align-items: center; position: relative; padding: 5px 16px; width: 100%; @@ -340,10 +354,6 @@ onBeforeUnmount(() => { pointer-events: none; font-size: 0.7em; padding-bottom: 4px; - - > span { - opacity: 0.7; - } } &.pending { @@ -373,6 +383,22 @@ onBeforeUnmount(() => { } } +.item_content { + width: 100%; + max-width: 100vw; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + text-overflow: ellipsis; +} + +.item_content_text { + max-width: calc(100vw - 4rem); + text-overflow: ellipsis; + overflow: hidden; +} + .switch { position: relative; display: flex; @@ -406,6 +432,7 @@ onBeforeUnmount(() => { .icon { margin-right: 8px; + line-height: 1; } .caret { @@ -419,9 +446,8 @@ onBeforeUnmount(() => { } .indicator { - position: absolute; - top: 5px; - left: 13px; + display: flex; + align-items: center; color: var(--indicator); font-size: 12px; animation: blink 1s infinite; diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue index 8d2a147306..f0a2c232bd 100644 --- a/packages/frontend/src/components/MkMiniChart.vue +++ b/packages/frontend/src/components/MkMiniChart.vue @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, ref } from 'vue'; import { v4 as uuid } from 'uuid'; import tinycolor from 'tinycolor2'; import { useInterval } from '@/scripts/use-interval.js'; @@ -43,11 +43,11 @@ const props = defineProps<{ const viewBoxX = 50; const viewBoxY = 50; const gradientId = uuid(); -let polylinePoints = $ref(''); -let polygonPoints = $ref(''); -let headX = $ref<number | null>(null); -let headY = $ref<number | null>(null); -let clock = $ref<number | null>(null); +const polylinePoints = ref(''); +const polygonPoints = ref(''); +const headX = ref<number | null>(null); +const headY = ref<number | null>(null); +const clock = ref<number | null>(null); const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent')); const color = accent.toRgbString(); @@ -60,12 +60,12 @@ function draw(): void { (1 - (n / peak)) * viewBoxY, ]); - polylinePoints = _polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); + polylinePoints.value = _polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - polygonPoints = `0,${ viewBoxY } ${ polylinePoints } ${ viewBoxX },${ viewBoxY }`; + polygonPoints.value = `0,${ viewBoxY } ${ polylinePoints.value } ${ viewBoxX },${ viewBoxY }`; - headX = _polylinePoints.at(-1)![0]; - headY = _polylinePoints.at(-1)![1]; + headX.value = _polylinePoints.at(-1)![0]; + headY.value = _polylinePoints.at(-1)![1]; } watch(() => props.src, draw, { immediate: true }); diff --git a/packages/frontend/src/components/MkModPlayer.vue b/packages/frontend/src/components/MkModPlayer.vue index c24eaab2fa..055522d466 100644 --- a/packages/frontend/src/components/MkModPlayer.vue +++ b/packages/frontend/src/components/MkModPlayer.vue @@ -29,7 +29,7 @@ </template> <script lang="ts" setup> -import { ref, nextTick } from 'vue'; +import { ref, nextTick, computed } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; @@ -71,9 +71,9 @@ const props = defineProps<{ module: Misskey.entities.DriveFile }>(); -const isSensitive = $computed(() => { return props.module.isSensitive; }); -const url = $computed(() => { return props.module.url; }); -let hide = ref((defaultStore.state.nsfw === 'force') ? true : isSensitive && (defaultStore.state.nsfw !== 'ignore')); +const isSensitive = computed(() => { return props.module.isSensitive; }); +const url = computed(() => { return props.module.url; }); +let hide = ref((defaultStore.state.nsfw === 'force') ? true : isSensitive.value && (defaultStore.state.nsfw !== 'ignore')); let playing = ref(false); let displayCanvas = ref<HTMLCanvasElement>(); let progress = ref<HTMLProgressElement>(); @@ -84,7 +84,7 @@ const rowBuffer = 24; let buffer = null; let isSeeking = false; -player.value.load(url).then((result) => { +player.value.load(url.value).then((result) => { buffer = result; try { player.value.play(buffer); diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index ec5039c504..5cd31cdf7c 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch } from 'vue'; +import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch, ref, shallowRef, computed } from 'vue'; import * as os from '@/os.js'; import { isTouchUsing } from '@/scripts/touch.js'; import { defaultStore } from '@/store.js'; @@ -89,14 +89,14 @@ const emit = defineEmits<{ provide('modal', true); -let maxHeight = $ref<number>(); -let fixed = $ref(false); -let transformOrigin = $ref('center'); -let showing = $ref(true); -let content = $shallowRef<HTMLElement>(); +const maxHeight = ref<number>(); +const fixed = ref(false); +const transformOrigin = ref('center'); +const showing = ref(true); +const content = shallowRef<HTMLElement>(); const zIndex = os.claimZIndex(props.zPriority); -let useSendAnime = $ref(false); -const type = $computed<ModalTypes>(() => { +const useSendAnime = ref(false); +const type = computed<ModalTypes>(() => { if (props.preferType === 'auto') { if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') { return 'drawer'; @@ -107,26 +107,26 @@ const type = $computed<ModalTypes>(() => { return props.preferType!; } }); -const isEnableBgTransparent = $computed(() => props.transparentBg && (type === 'popup')); -let transitionName = $computed((() => +const isEnableBgTransparent = computed(() => props.transparentBg && (type.value === 'popup')); +const transitionName = computed((() => defaultStore.state.animation - ? useSendAnime + ? useSendAnime.value ? 'send' - : type === 'drawer' + : type.value === 'drawer' ? 'modal-drawer' - : type === 'popup' + : type.value === 'popup' ? 'modal-popup' : 'modal' : '' )); -let transitionDuration = $computed((() => - transitionName === 'send' +const transitionDuration = computed((() => + transitionName.value === 'send' ? 400 - : transitionName === 'modal-popup' + : transitionName.value === 'modal-popup' ? 100 - : transitionName === 'modal' + : transitionName.value === 'modal' ? 200 - : transitionName === 'modal-drawer' + : transitionName.value === 'modal-drawer' ? 200 : 0 )); @@ -135,12 +135,12 @@ let contentClicking = false; function close(opts: { useSendAnimation?: boolean } = {}) { if (opts.useSendAnimation) { - useSendAnime = true; + useSendAnime.value = true; } // eslint-disable-next-line vue/no-mutating-props if (props.src) props.src.style.pointerEvents = 'auto'; - showing = false; + showing.value = false; emit('close'); } @@ -149,8 +149,8 @@ function onBgClick() { emit('click'); } -if (type === 'drawer') { - maxHeight = window.innerHeight / 1.5; +if (type.value === 'drawer') { + maxHeight.value = window.innerHeight / 1.5; } const keymap = { @@ -162,21 +162,21 @@ const SCROLLBAR_THICKNESS = 16; const align = () => { if (props.src == null) return; - if (type === 'drawer') return; - if (type === 'dialog') return; + if (type.value === 'drawer') return; + if (type.value === 'dialog') return; - if (content == null) return; + if (content.value == null) return; const srcRect = props.src.getBoundingClientRect(); - const width = content!.offsetWidth; - const height = content!.offsetHeight; + const width = content.value!.offsetWidth; + const height = content.value!.offsetHeight; let left; let top; - const x = srcRect.left + (fixed ? 0 : window.pageXOffset); - const y = srcRect.top + (fixed ? 0 : window.pageYOffset); + const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset); + const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset); if (props.anchor.x === 'center') { left = x + (props.src.offsetWidth / 2) - (width / 2); @@ -194,7 +194,7 @@ const align = () => { top = y + props.src.offsetHeight; } - if (fixed) { + if (fixed.value) { // 画面から横にはみ出る場合 if (left + width > (window.innerWidth - SCROLLBAR_THICKNESS)) { left = (window.innerWidth - SCROLLBAR_THICKNESS) - width; @@ -207,16 +207,16 @@ const align = () => { if (top + height > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) { if (props.noOverlap && props.anchor.x === 'center') { if (underSpace >= (upperSpace / 3)) { - maxHeight = underSpace; + maxHeight.value = underSpace; } else { - maxHeight = upperSpace; + maxHeight.value = upperSpace; top = (upperSpace + MARGIN) - height; } } else { top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height; } } else { - maxHeight = underSpace; + maxHeight.value = underSpace; } } else { // 画面から横にはみ出る場合 @@ -231,16 +231,16 @@ const align = () => { if (top + height - window.pageYOffset > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) { if (props.noOverlap && props.anchor.x === 'center') { if (underSpace >= (upperSpace / 3)) { - maxHeight = underSpace; + maxHeight.value = underSpace; } else { - maxHeight = upperSpace; + maxHeight.value = upperSpace; top = window.pageYOffset + ((upperSpace + MARGIN) - height); } } else { top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.pageYOffset - 1; } } else { - maxHeight = underSpace; + maxHeight.value = underSpace; } } @@ -255,29 +255,29 @@ const align = () => { let transformOriginX = 'center'; let transformOriginY = 'center'; - if (top >= srcRect.top + props.src.offsetHeight + (fixed ? 0 : window.pageYOffset)) { + if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.pageYOffset)) { transformOriginY = 'top'; - } else if ((top + height) <= srcRect.top + (fixed ? 0 : window.pageYOffset)) { + } else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.pageYOffset)) { transformOriginY = 'bottom'; } - if (left >= srcRect.left + props.src.offsetWidth + (fixed ? 0 : window.pageXOffset)) { + if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.pageXOffset)) { transformOriginX = 'left'; - } else if ((left + width) <= srcRect.left + (fixed ? 0 : window.pageXOffset)) { + } else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.pageXOffset)) { transformOriginX = 'right'; } - transformOrigin = `${transformOriginX} ${transformOriginY}`; + transformOrigin.value = `${transformOriginX} ${transformOriginY}`; - content.style.left = left + 'px'; - content.style.top = top + 'px'; + content.value.style.left = left + 'px'; + content.value.style.top = top + 'px'; }; const onOpened = () => { emit('opened'); // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する - const el = content!.children[0]; + const el = content.value!.children[0]; el.addEventListener('mousedown', ev => { contentClicking = true; window.addEventListener('mouseup', ev => { @@ -299,7 +299,7 @@ onMounted(() => { // eslint-disable-next-line vue/no-mutating-props props.src.style.pointerEvents = 'none'; } - fixed = (type === 'drawer') || (getFixedContainer(props.src) != null); + fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null); await nextTick(); @@ -307,7 +307,7 @@ onMounted(() => { }, { immediate: true }); nextTick(() => { - alignObserver.observe(content!); + alignObserver.observe(content.value!); }); }); diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index 800950ea82..b91988304d 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted } from 'vue'; +import { onMounted, onUnmounted, shallowRef, ref } from 'vue'; import MkModal from './MkModal.vue'; const props = withDefaults(defineProps<{ @@ -44,14 +44,14 @@ const emit = defineEmits<{ (event: 'ok'): void; }>(); -let modal = $shallowRef<InstanceType<typeof MkModal>>(); -let rootEl = $shallowRef<HTMLElement>(); -let headerEl = $shallowRef<HTMLElement>(); -let bodyWidth = $ref(0); -let bodyHeight = $ref(0); +const modal = shallowRef<InstanceType<typeof MkModal>>(); +const rootEl = shallowRef<HTMLElement>(); +const headerEl = shallowRef<HTMLElement>(); +const bodyWidth = ref(0); +const bodyHeight = ref(0); const close = () => { - modal.close(); + modal.value.close(); }; const onBgClick = () => { @@ -67,14 +67,14 @@ const onKeydown = (evt) => { }; const ro = new ResizeObserver((entries, observer) => { - bodyWidth = rootEl.offsetWidth; - bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight; + bodyWidth.value = rootEl.value.offsetWidth; + bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight; }); onMounted(() => { - bodyWidth = rootEl.offsetWidth; - bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight; - ro.observe(rootEl); + bodyWidth.value = rootEl.value.offsetWidth; + bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight; + ro.observe(rootEl.value); }); onUnmounted(() => { diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 74edc8903e..9ecf21071d 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div - v-if="!muted" + v-if="!hardMuted && !muted" v-show="!isDeleted" ref="el" v-hotkey="keymap" @@ -50,14 +50,14 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> <MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/> <div :class="[$style.main, { [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined"> - <MkNoteHeader :note="appearNote" :mini="true" v-on:click.stop/> + <MkNoteHeader :note="appearNote" :mini="true" @click.stop/> <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> <div style="container-type: inline-size;"> <p v-if="appearNote.cw != null" :class="$style.cw"> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> - <MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;" v-on:click.stop/> + <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/> </p> - <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]" > + <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> <div :class="$style.text"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA> @@ -79,31 +79,31 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> </div> </div> - <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> - <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> + <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> + <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> </div> <div v-if="appearNote.files.length > 0"> - <MkMediaList :mediaList="appearNote.files" v-on:click.stop/> + <MkMediaList :mediaList="appearNote.files" @click.stop/> </div> - <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" v-on:click.stop /> - <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" v-on:click.stop/> + <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" @click.stop/> + <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" @click.stop/> <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> - <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" v-on:click.stop @click="collapsed = false"> + <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false"> <span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span> </button> - <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" v-on:click.stop @click="collapsed = true"> + <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click.stop @click="collapsed = true"> <span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span> </button> </div> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> </div> - <MkReactionsViewer :note="appearNote" :maxNumber="16" v-on:click.stop @mockUpdateMyReaction="emitUpdReaction"> + <MkReactionsViewer :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction"> <template #more> <div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div> </template> </MkReactionsViewer> <footer :class="$style.footer"> - <button :class="$style.footerButton" class="_button" v-on:click.stop @click="reply()"> + <button :class="$style.footerButton" class="_button" @click.stop @click="reply()"> <i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p> </button> @@ -113,7 +113,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="$style.footerButton" class="_button" :style="renoted ? 'color: var(--accent) !important;' : ''" - v-on:click.stop + @click.stop @mousedown="renoted ? undoRenote(appearNote) : boostVisibility()" > <i class="ph-rocket-launch ph-bold ph-lg"></i> @@ -127,19 +127,19 @@ SPDX-License-Identifier: AGPL-3.0-only ref="quoteButton" :class="$style.footerButton" class="_button" - v-on:click.stop + @click.stop @mousedown="quote()" > <i class="ph-quotes ph-bold ph-lg"></i> </button> - <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" v-on:click.stop @click="like()"> + <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()"> <i class="ph-heart ph-bold ph-lg"></i> </button> <button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()"> <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> </button> - <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" v-on:click.stop @click="undoReact(appearNote)"> + <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click.stop @click="undoReact(appearNote)"> <i class="ph-minus ph-bold ph-lg"></i> </button> <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()"> @@ -152,7 +152,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </article> </div> -<div v-else :class="$style.muted" @click="muted = false"> +<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false"> <I18n :src="i18n.ts.userSaysSomething" tag="small"> <template #name> <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)"> @@ -161,10 +161,16 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </I18n> </div> +<div v-else> + <!-- + MkDateSeparatedList uses TransitionGroup which requires single element in the child elements + so MkNote create empty div instead of no elements + --> +</div> </template> <script lang="ts" setup> -import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent, watch, provide } from 'vue'; +import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue'; import * as mfm from '@sharkey/sfm-js'; import * as Misskey from 'misskey-js'; import MkNoteSub from '@/components/MkNoteSub.vue'; @@ -183,6 +189,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 * as sound from '@/scripts/sound.js'; import { defaultStore, noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; @@ -206,6 +213,7 @@ const props = withDefaults(defineProps<{ note: Misskey.entities.Note; pinned?: boolean; mock?: boolean; + withHardMute?: boolean; }>(), { mock: false, }); @@ -222,7 +230,7 @@ const router = useRouter(); const inChannel = inject('inChannel', null); const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); -let note = $ref(deepClone(props.note)); +const note = ref(deepClone(props.note)); function noteclick(id: string) { const selection = document.getSelection(); @@ -234,7 +242,7 @@ function noteclick(id: string) { // plugin if (noteViewInterruptors.length > 0) { onMounted(async () => { - let result: Misskey.entities.Note | null = deepClone(note); + let result: Misskey.entities.Note | null = deepClone(note.value); for (const interruptor of noteViewInterruptors) { try { result = await interruptor.handler(result); @@ -246,15 +254,16 @@ if (noteViewInterruptors.length > 0) { console.error(err); } } - note = result; + note.value = result; }); } const isRenote = ( - note.renote != null && - note.text == null && - note.fileIds.length === 0 && - note.poll == null + note.value.renote != null && + note.value.text == null && + note.value.cw == null && + note.value.fileIds.length === 0 && + note.value.poll == null ); const el = shallowRef<HTMLElement>(); @@ -266,27 +275,37 @@ const reactButton = shallowRef<HTMLElement>(); const quoteButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>(); const likeButton = shallowRef<HTMLElement>(); -let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note); -const renoteUrl = appearNote.renote ? appearNote.renote.url : null; -const renoteUri = appearNote.renote ? appearNote.renote.uri : null; +const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null; +const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null; -const isMyRenote = $i && ($i.id === note.userId); +const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(defaultStore.state.uncollapseCW); -const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null); -const urls = $computed(() => parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null); -const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null); -const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); -const isLong = shouldCollapsed(appearNote, urls ?? []); -const collapsed = defaultStore.state.expandLongNote && appearNote.cw == null ? false : ref(appearNote.cw == null && isLong); +const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text).filter(u => u !== renoteUrl && u !== renoteUri) : null); +const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null); +const isLong = shouldCollapsed(appearNote.value, urls.value ?? []); +const collapsed = defaultStore.state.expandLongNote && appearNote.value.cw == null ? false : ref(appearNote.value.cw == null && isLong); const isDeleted = ref(false); const renoted = ref(false); -const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); +const muted = ref(checkMute(appearNote.value, $i?.mutedWords)); +const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords)); const translation = ref<any>(null); const translating = ref(false); -const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); -const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id)); -let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null))); +const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); +const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id)); +const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null))); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); +const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null); +const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); + +function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean { + if (mutedWords == null) return false; + + if (checkWordMute(note, $i, mutedWords)) return true; + if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true; + if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true; + return false; +} const keymap = { 'r': () => reply(true), @@ -301,20 +320,20 @@ const keymap = { provide('react', (reaction: string) => { os.api('notes/reactions/create', { - noteId: appearNote.id, + noteId: appearNote.value.id, reaction: reaction, }); }); if (props.mock) { watch(() => props.note, (to) => { - note = deepClone(to); + note.value = deepClone(to); }, { deep: true }); } else { useNoteCapture({ rootEl: el, - note: $$(appearNote), - pureNote: $$(note), + note: appearNote, + pureNote: note, isDeletedRef: isDeleted, }); } @@ -322,7 +341,7 @@ if (props.mock) { if (!props.mock) { useTooltip(renoteButton, async (showing) => { const renotes = await os.api('notes/renotes', { - noteId: appearNote.id, + noteId: appearNote.value.id, limit: 11, }); @@ -333,14 +352,14 @@ if (!props.mock) { os.popup(MkUsersTooltip, { showing, users, - count: appearNote.renoteCount, + count: appearNote.value.renoteCount, targetElement: renoteButton.value, }, {}, 'closed'); }); useTooltip(quoteButton, async (showing) => { const renotes = await os.api('notes/renotes', { - noteId: appearNote.id, + noteId: appearNote.value.id, limit: 11, quote: true, }); @@ -352,14 +371,14 @@ if (!props.mock) { os.popup(MkUsersTooltip, { showing, users, - count: appearNote.renoteCount, + count: appearNote.value.renoteCount, targetElement: quoteButton.value, }, {}, 'closed'); }); if ($i) { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, }).then((res) => { @@ -419,7 +438,7 @@ function renote(visibility: Visibility | 'local') { pleaseLogin(); showMovedDialog(); - if (appearNote.channel) { + if (appearNote.value.channel) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -430,14 +449,14 @@ function renote(visibility: Visibility | 'local') { if (!props.mock) { os.api('notes/create', { - renoteId: appearNote.id, - channelId: appearNote.channelId, + renoteId: appearNote.value.id, + channelId: appearNote.value.channelId, }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; }); } - } else if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) { + } else if (!appearNote.value.channel || appearNote.value.channel?.allowRenoteToExternal) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -449,16 +468,16 @@ function renote(visibility: Visibility | 'local') { const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility; const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly; - let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility); - if (appearNote.channel?.isSensitive) { - noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.visibility : visibility, 'home'); + let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility); + if (appearNote.value.channel?.isSensitive) { + noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home'); } if (!props.mock) { os.api('notes/create', { localOnly: visibility === 'local' ? true : localOnlySetting, visibility: noteVisibility, - renoteId: appearNote.id, + renoteId: appearNote.value.id, }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; @@ -474,13 +493,13 @@ function quote() { return; } - if (appearNote.channel) { + if (appearNote.value.channel) { os.post({ - renote: appearNote, - channel: appearNote.channel, + renote: appearNote.value, + channel: appearNote.value.channel, }).then(() => { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, quote: true, @@ -499,10 +518,10 @@ function quote() { }); } else { os.post({ - renote: appearNote, + renote: appearNote.value, }).then(() => { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, quote: true, @@ -528,8 +547,8 @@ function reply(viaKeyboard = false): void { return; } os.post({ - reply: appearNote, - channel: appearNote.channel, + reply: appearNote.value, + channel: appearNote.value.channel, animation: !viaKeyboard, }, () => { focus(); @@ -543,7 +562,7 @@ function like(): void { return; } os.api('notes/like', { - noteId: appearNote.id, + noteId: appearNote.value.id, override: defaultLike.value, }); const el = likeButton.value as HTMLElement | null | undefined; @@ -558,13 +577,15 @@ function like(): void { function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); - if (appearNote.reactionAcceptance === 'likeOnly') { + if (appearNote.value.reactionAcceptance === 'likeOnly') { + sound.play('reaction'); + if (props.mock) { return; } os.api('notes/like', { - noteId: appearNote.id, + noteId: appearNote.value.id, override: defaultLike.value, }); const el = reactButton.value as HTMLElement | null | undefined; @@ -577,16 +598,18 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { + sound.play('reaction'); + if (props.mock) { emit('reaction', reaction); return; } os.api('notes/reactions/create', { - noteId: appearNote.id, + noteId: appearNote.value.id, reaction: reaction, }); - if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { + if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) { claimAchievement('reactWithoutRead'); } }, () => { @@ -613,8 +636,8 @@ function undoRenote(note) : void { if (props.mock) { return; } - os.api("notes/unrenote", { - noteId: note.id + os.api('notes/unrenote', { + noteId: note.id, }); os.toast(i18n.ts.rmboost); renoted.value = false; @@ -648,7 +671,7 @@ function onContextmenu(ev: MouseEvent): void { ev.preventDefault(); react(); } else { - const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); + const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); os.contextMenu(menu, ev).then(focus).finally(cleanup); } } @@ -658,14 +681,14 @@ function menu(viaKeyboard = false): void { return; } - const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); + const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); os.popupMenu(menu, menuButton.value, { viaKeyboard, }).then(focus).finally(cleanup); } async function menuVersions(viaKeyboard = false): Promise<void> { - const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton }); + const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton }); os.popupMenu(menu, menuVersionsButton.value, { viaKeyboard, }).then(focus).finally(cleanup); @@ -676,7 +699,7 @@ async function clip() { return; } - os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); + os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); } function showRenoteMenu(viaKeyboard = false): void { @@ -691,7 +714,7 @@ function showRenoteMenu(viaKeyboard = false): void { danger: true, action: () => { os.api('notes/delete', { - noteId: note.id, + noteId: note.value.id, }); isDeleted.value = true; }, @@ -701,17 +724,17 @@ function showRenoteMenu(viaKeyboard = false): void { if (isMyRenote) { pleaseLogin(); os.popupMenu([ - getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote), - null, + getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), + { type: 'divider' }, getUnrenote(), ], renoteTime.value, { viaKeyboard: viaKeyboard, }); } else { os.popupMenu([ - getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote), - null, - getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote), + getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), + { type: 'divider' }, + getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote), $i.isModerator || $i.isAdmin ? getUnrenote() : undefined, ], renoteTime.value, { viaKeyboard: viaKeyboard, @@ -749,7 +772,7 @@ function focusAfter() { function readPromo() { os.api('promo/read', { - noteId: appearNote.id, + noteId: appearNote.value.id, }); isDeleted.value = true; } diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 93e39ff033..f29b9db6ae 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.noteContent"> <p v-if="appearNote.cw != null" :class="$style.cw"> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> - <MkCwButton v-model="showContent" :note="appearNote"/> + <MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/> </p> <div v-show="appearNote.cw == null || showContent"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> @@ -93,8 +93,8 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> </div> </div> - <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> - <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> + <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> + <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> <div v-if="appearNote.files.length > 0"> <MkMediaList :mediaList="appearNote.files"/> </div> @@ -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 * as sound from '@/scripts/sound.js'; import { defaultStore, noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; @@ -248,12 +249,11 @@ import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import { claimAchievement } from '@/scripts/achievements.js'; -import { MenuItem } from '@/types/menu.js'; import { checkAnimationFromMfm } from '@/scripts/check-animated-mfm.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; -import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import MkPagination from '@/components/MkPagination.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; @@ -264,12 +264,12 @@ const props = defineProps<{ const inChannel = inject('inChannel', null); -let note = $ref(deepClone(props.note)); +const note = ref(deepClone(props.note)); // plugin if (noteViewInterruptors.length > 0) { onMounted(async () => { - let result: Misskey.entities.Note | null = deepClone(note); + let result: Misskey.entities.Note | null = deepClone(note.value); for (const interruptor of noteViewInterruptors) { try { result = await interruptor.handler(result); @@ -281,15 +281,15 @@ if (noteViewInterruptors.length > 0) { console.error(err); } } - note = result; + note.value = result; }); } const isRenote = ( - note.renote != null && - note.text == null && - note.fileIds.length === 0 && - note.poll == null + note.value.renote != null && + note.value.text == null && + note.value.fileIds.length === 0 && + note.value.poll == null ); const el = shallowRef<HTMLElement>(); @@ -301,26 +301,25 @@ const reactButton = shallowRef<HTMLElement>(); const quoteButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>(); const likeButton = shallowRef<HTMLElement>(); -let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note); -const renoteUrl = appearNote.renote ? appearNote.renote.url : null; -const renoteUri = appearNote.renote ? appearNote.renote.uri : null; - -const isMyRenote = $i && ($i.id === note.userId); +const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null; +const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null; +const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(defaultStore.state.uncollapseCW); const isDeleted = ref(false); const renoted = ref(false); -const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); +const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false); const translation = ref(null); const translating = ref(false); -const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null); +const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; const urls = parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null; -const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null); +const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null); const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); -const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); +const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); const conversation = ref<Misskey.entities.Note[]>([]); const replies = ref<Misskey.entities.Note[]>([]); const quotes = ref<Misskey.entities.Note[]>([]); -const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id); +const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i.id); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); watch(() => props.expandAllCws, (expandAllCws) => { @@ -328,8 +327,8 @@ watch(() => props.expandAllCws, (expandAllCws) => { }); if ($i) { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, }).then((res) => { @@ -348,41 +347,41 @@ const keymap = { provide('react', (reaction: string) => { os.api('notes/reactions/create', { - noteId: appearNote.id, + noteId: appearNote.value.id, reaction: reaction, }); }); -let tab = $ref('replies'); -let reactionTabType = $ref(null); +const tab = ref('replies'); +const reactionTabType = ref(null); -const renotesPagination = $computed(() => ({ +const renotesPagination = computed(() => ({ endpoint: 'notes/renotes', limit: 10, params: { - noteId: appearNote.id, + noteId: appearNote.value.id, }, })); -const reactionsPagination = $computed(() => ({ +const reactionsPagination = computed(() => ({ endpoint: 'notes/reactions', limit: 10, params: { - noteId: appearNote.id, - type: reactionTabType, + noteId: appearNote.value.id, + type: reactionTabType.value, }, })); useNoteCapture({ rootEl: el, - note: $$(appearNote), - pureNote: $$(note), + note: appearNote, + pureNote: note, isDeletedRef: isDeleted, }); useTooltip(renoteButton, async (showing) => { const renotes = await os.api('notes/renotes', { - noteId: appearNote.id, + noteId: appearNote.value.id, limit: 11, }); @@ -393,14 +392,14 @@ useTooltip(renoteButton, async (showing) => { os.popup(MkUsersTooltip, { showing, users, - count: appearNote.renoteCount, + count: appearNote.value.renoteCount, targetElement: renoteButton.value, }, {}, 'closed'); }); useTooltip(quoteButton, async (showing) => { const renotes = await os.api('notes/renotes', { - noteId: appearNote.id, + noteId: appearNote.value.id, limit: 11, quote: true, }); @@ -412,7 +411,7 @@ useTooltip(quoteButton, async (showing) => { os.popup(MkUsersTooltip, { showing, users, - count: appearNote.renoteCount, + count: appearNote.value.renoteCount, targetElement: quoteButton.value, }, {}, 'closed'); }); @@ -467,7 +466,7 @@ function renote(visibility: Visibility | 'local') { pleaseLogin(); showMovedDialog(); - if (appearNote.channel) { + if (appearNote.value.channel) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -477,13 +476,13 @@ function renote(visibility: Visibility | 'local') { } os.api('notes/create', { - renoteId: appearNote.id, - channelId: appearNote.channelId, + renoteId: appearNote.value.id, + channelId: appearNote.value.channelId, }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; }); - } else if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) { + } else if (!appearNote.value.channel || appearNote.value.channel?.allowRenoteToExternal) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -495,15 +494,15 @@ function renote(visibility: Visibility | 'local') { const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility; const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly; - let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility); - if (appearNote.channel?.isSensitive) { - noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.visibility : visibility, 'home'); + let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility); + if (appearNote.value.channel?.isSensitive) { + noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home'); } os.api('notes/create', { localOnly: visibility === 'local' ? true : localOnlySetting, visibility: noteVisibility, - renoteId: appearNote.id, + renoteId: appearNote.value.id, }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; @@ -515,13 +514,13 @@ function quote() { pleaseLogin(); showMovedDialog(); - if (appearNote.channel) { + if (appearNote.value.channel) { os.post({ - renote: appearNote, - channel: appearNote.channel, + renote: appearNote.value, + channel: appearNote.value.channel, }).then(() => { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, quote: true, @@ -540,10 +539,10 @@ function quote() { }); } else { os.post({ - renote: appearNote, + renote: appearNote.value, }).then(() => { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, quote: true, @@ -567,8 +566,8 @@ function reply(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); os.post({ - reply: appearNote, - channel: appearNote.channel, + reply: appearNote.value, + channel: appearNote.value.channel, animation: !viaKeyboard, }, () => { focus(); @@ -578,9 +577,9 @@ function reply(viaKeyboard = false): void { function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); - if (appearNote.reactionAcceptance === 'likeOnly') { + if (appearNote.value.reactionAcceptance === 'likeOnly') { os.api('notes/like', { - noteId: appearNote.id, + noteId: appearNote.value.id, override: defaultLike.value, }); const el = reactButton.value as HTMLElement | null | undefined; @@ -593,11 +592,13 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { + sound.play('reaction'); + os.api('notes/reactions/create', { - noteId: appearNote.id, + noteId: appearNote.value.id, reaction: reaction, }); - if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { + if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) { claimAchievement('reactWithoutRead'); } }, () => { @@ -610,7 +611,7 @@ function like(): void { pleaseLogin(); showMovedDialog(); os.api('notes/like', { - noteId: appearNote.id, + noteId: appearNote.value.id, override: defaultLike.value, }); const el = likeButton.value as HTMLElement | null | undefined; @@ -632,8 +633,8 @@ function undoReact(note): void { function undoRenote() : void { if (!renoted.value) return; - os.api("notes/unrenote", { - noteId: appearNote.id, + os.api('notes/unrenote', { + noteId: appearNote.value.id, }); os.toast(i18n.ts.rmboost); renoted.value = false; @@ -661,27 +662,27 @@ function onContextmenu(ev: MouseEvent): void { ev.preventDefault(); react(); } else { - const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }); + const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted }); os.contextMenu(menu, ev).then(focus).finally(cleanup); } } function menu(viaKeyboard = false): void { - const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }); + const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted }); os.popupMenu(menu, menuButton.value, { viaKeyboard, }).then(focus).finally(cleanup); } async function menuVersions(viaKeyboard = false): Promise<void> { - const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton }); + const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton }); os.popupMenu(menu, menuVersionsButton.value, { viaKeyboard, }).then(focus).finally(cleanup); } async function clip() { - os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus); + os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus); } function showRenoteMenu(viaKeyboard = false): void { @@ -693,7 +694,7 @@ function showRenoteMenu(viaKeyboard = false): void { danger: true, action: () => { os.api('notes/delete', { - noteId: note.id, + noteId: note.value.id, }); isDeleted.value = true; }, @@ -715,7 +716,7 @@ const repliesLoaded = ref(false); function loadReplies() { repliesLoaded.value = true; os.api('notes/children', { - noteId: appearNote.id, + noteId: appearNote.value.id, limit: 30, showQuotes: false, }).then(res => { @@ -730,7 +731,7 @@ const quotesLoaded = ref(false); function loadQuotes() { quotesLoaded.value = true; os.api('notes/renotes', { - noteId: appearNote.id, + noteId: appearNote.value.id, limit: 30, quote: true, }).then(res => { @@ -745,13 +746,13 @@ const conversationLoaded = ref(false); function loadConversation() { conversationLoaded.value = true; os.api('notes/conversation', { - noteId: appearNote.replyId, + noteId: appearNote.value.replyId, }).then(res => { conversation.value = res.reverse(); }); } -if (appearNote.reply && appearNote.reply.replyId && defaultStore.state.autoloadConversation) loadConversation(); +if (appearNote.value.reply && appearNote.value.reply.replyId && defaultStore.state.autoloadConversation) loadConversation(); function animatedMFM() { if (allowAnim.value) { diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue index 552f8137ed..c517bc6800 100644 --- a/packages/frontend/src/components/MkNotePreview.vue +++ b/packages/frontend/src/components/MkNotePreview.vue @@ -11,7 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only <MkUserName :user="user" :nowrap="true"/> </div> <div> - <div> + <p v-if="useCw" :class="$style.cw"> + <Mfm v-if="cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/> + <MkCwButton v-model="showContent" :text="text.trim()" :files="files" :poll="poll" style="margin: 4px 0;"/> + </p> + <div v-show="!useCw || showContent"> <Mfm :text="text.trim()" :author="user" :nyaize="'respect'" :i="user"/> </div> </div> @@ -20,11 +24,23 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import * as Misskey from 'misskey-js'; +import MkCwButton from '@/components/MkCwButton.vue'; + +const showContent = ref(false); const props = defineProps<{ text: string; + files: Misskey.entities.DriveFile[]; + poll?: { + choices: string[]; + multiple: boolean; + expiresAt: string | null; + expiredAfter: string | null; + }; + useCw: boolean; + cw: string | null; user: Misskey.entities.User; }>(); </script> @@ -53,6 +69,14 @@ const props = defineProps<{ min-width: 0; } +.cw { + cursor: default; + display: block; + margin: 0; + padding: 0; + overflow-wrap: break-word; +} + .header { margin-bottom: 2px; font-weight: bold; diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index bc0f82d44d..7a6109ee0b 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div> <p v-if="note.cw != null" :class="$style.cw"> <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> - <MkCwButton v-model="showContent" :note="note" v-on:click.stop/> + <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll" @click.stop/> </p> <div v-show="note.cw == null || showContent"> <MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note"/> @@ -22,12 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkCwButton from '@/components/MkCwButton.vue'; -import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; const props = defineProps<{ @@ -36,10 +35,10 @@ const props = defineProps<{ hideFiles?: boolean; }>(); -let showContent = $ref(defaultStore.state.uncollapseCW); +let showContent = ref(defaultStore.state.uncollapseCW); watch(() => props.expandAllCws, (expandAllCws) => { - if (expandAllCws !== showContent) showContent = expandAllCws; + if (expandAllCws !== showContent.value) showContent.value = expandAllCws; }); </script> diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 5b1e1af308..8d394c0c15 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.content"> <p v-if="note.cw != null" :class="$style.cw"> <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/> - <MkCwButton v-model="showContent" :note="note"/> + <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/> </p> <div v-show="note.cw == null || showContent"> <MkSubNoteContent :class="$style.text" :note="note" :translating="translating" :translation="translation"/> @@ -93,15 +93,14 @@ import { notePage } from '@/filters/note.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { userPage } from "@/filters/user.js"; -import { checkWordMute } from "@/scripts/check-word-mute.js"; -import { defaultStore } from "@/store.js"; +import { userPage } from '@/filters/user.js'; +import { checkWordMute } from '@/scripts/check-word-mute.js'; +import { defaultStore } from '@/store.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { claimAchievement } from '@/scripts/achievements.js'; -import type { MenuItem } from '@/types/menu.js'; import { getNoteMenu } from '@/scripts/get-note-menu.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js'; @@ -131,7 +130,7 @@ const quoteButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>(); const likeButton = shallowRef<HTMLElement>(); -let appearNote = $computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note); +let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); const isRenote = ( @@ -143,13 +142,13 @@ const isRenote = ( useNoteCapture({ rootEl: el, - note: $$(appearNote), + note: appearNote, isDeletedRef: isDeleted, }); if ($i) { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, }).then((res) => { @@ -230,8 +229,8 @@ function undoReact(note): void { function undoRenote() : void { if (!renoted.value) return; - os.api("notes/unrenote", { - noteId: appearNote.id, + os.api('notes/unrenote', { + noteId: appearNote.value.id, }); os.toast(i18n.ts.rmboost); renoted.value = false; @@ -245,13 +244,13 @@ function undoRenote() : void { } } -let showContent = $ref(defaultStore.state.uncollapseCW); +let showContent = ref(defaultStore.state.uncollapseCW); watch(() => props.expandAllCws, (expandAllCws) => { - if (expandAllCws !== showContent) showContent = expandAllCws; + if (expandAllCws !== showContent.value) showContent.value = expandAllCws; }); -let replies: Misskey.entities.Note[] = $ref([]); +let replies = ref<Misskey.entities.Note[]>([]); function boostVisibility() { os.popupMenu([ @@ -293,7 +292,7 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc pleaseLogin(); showMovedDialog(); - if (appearNote.channel) { + if (appearNote.value.channel) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -333,12 +332,12 @@ function quote() { pleaseLogin(); showMovedDialog(); - if (appearNote.channel) { + if (appearNote.value.channel) { os.post({ - renote: appearNote, - channel: appearNote.channel, + renote: appearNote.value, + channel: appearNote.value.channel, }).then(() => { - os.api("notes/renotes", { + os.api('notes/renotes', { noteId: props.note.id, userId: $i.id, limit: 1, @@ -358,9 +357,9 @@ function quote() { }); } else { os.post({ - renote: appearNote, + renote: appearNote.value, }).then(() => { - os.api("notes/renotes", { + os.api('notes/renotes', { noteId: props.note.id, userId: $i.id, limit: 1, @@ -394,7 +393,7 @@ if (props.detail) { limit: numberOfReplies.value, showQuotes: false, }).then(res => { - replies = res; + replies.value = res; }); } </script> diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index 0d2f0020d1..fc1c8a0f09 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only :ad="true" :class="$style.notes" > - <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/> + <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/> </MkDateSeparatedList> <MkDateSeparatedList v-else-if="defaultStore.state.noteDesign === 'sharkey'" diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index ae5be0f2d4..2901139220 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.head"> <MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/> + <MkAvatar v-else-if="notification.type === 'roleAssigned'" :class="$style.icon" :user="$i" link preview/> <MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div> @@ -36,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="notification.type === 'quote'" class="ph-quotes ph-bold ph-lg"></i> <i v-else-if="notification.type === 'pollEnded'" class="ph-chart-bar-horizontal ph-bold ph-lg"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ph-trophy ph-bold ph-lg"></i> + <img v-else-if="notification.type === 'roleAssigned'" :src="notification.role.iconUrl" alt=""/> <!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <MkReactionIcon v-else-if="notification.type === 'reaction'" @@ -50,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only <header :class="$style.header"> <span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span> <span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span> + <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> @@ -86,6 +89,9 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/> <i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i> </MkA> + <div v-else-if="notification.type === 'roleAssigned'" :class="$style.text"> + {{ notification.role.name }} + </div> <MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements"> {{ i18n.ts._achievements._types['_' + notification.achievement].title }} </MkA> @@ -130,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef } from 'vue'; +import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue index 3d5a56975b..6725776f43 100644 --- a/packages/frontend/src/components/MkNotificationSelectWindow.vue +++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, Ref } from 'vue'; +import { ref, Ref, shallowRef } from 'vue'; import MkSwitch from './MkSwitch.vue'; import MkInfo from './MkInfo.vue'; import MkButton from './MkButton.vue'; @@ -51,7 +51,7 @@ const props = withDefaults(defineProps<{ excludeTypes: () => [], }); -const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any); @@ -61,7 +61,7 @@ function ok() { .filter(type => !typesMap[type].value), }); - if (dialog) dialog.close(); + if (dialog.value) dialog.value.close(); } function disableAll() { diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index bfe668a165..a157820d56 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -15,11 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template #default="{ items: notifications }"> <MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'misskey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> - <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/> + <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/> <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> </MkDateSeparatedList> <MkDateSeparatedList v-else-if="defaultStore.state.noteDesign === 'sharkey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> - <SkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/> + <SkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/> <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> </MkDateSeparatedList> </template> @@ -29,13 +29,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue'; -import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import MkPagination from '@/components/MkPagination.vue'; import XNotification from '@/components/MkNotification.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkNote from '@/components/MkNote.vue'; import SkNote from '@/components/SkNote.vue'; import { useStream } from '@/stream.js'; -import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { notificationTypes } from '@/const.js'; import { infoImageUrl } from '@/instance.js'; @@ -48,7 +47,7 @@ const props = defineProps<{ const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); -const pagination: Paging = defaultStore.state.useGroupedNotifications ? { +const pagination = computed(() => defaultStore.reactiveState.useGroupedNotifications.value ? { endpoint: 'i/notifications-grouped' as const, limit: 20, params: computed(() => ({ @@ -60,7 +59,7 @@ const pagination: Paging = defaultStore.state.useGroupedNotifications ? { params: computed(() => ({ excludeTypes: props.excludeTypes ?? undefined, })), -}; +}); function onNotification(notification) { const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false; diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue index ac957d93dc..702bb95dc7 100644 --- a/packages/frontend/src/components/MkOmit.vue +++ b/packages/frontend/src/components/MkOmit.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted } from 'vue'; +import { onMounted, onUnmounted, shallowRef, ref } from 'vue'; import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ @@ -22,13 +22,13 @@ const props = withDefaults(defineProps<{ maxHeight: 200, }); -let content = $shallowRef<HTMLElement>(); -let omitted = $ref(false); -let ignoreOmit = $ref(false); +const content = shallowRef<HTMLElement>(); +const omitted = ref(false); +const ignoreOmit = ref(false); const calcOmit = () => { - if (omitted || ignoreOmit) return; - omitted = content.offsetHeight > props.maxHeight; + if (omitted.value || ignoreOmit.value) return; + omitted.value = content.value.offsetHeight > props.maxHeight; }; const omitObserver = new ResizeObserver((entries, observer) => { @@ -37,7 +37,7 @@ const omitObserver = new ResizeObserver((entries, observer) => { onMounted(() => { calcOmit(); - omitObserver.observe(content); + omitObserver.observe(content.value); }); onUnmounted(() => { diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue index 05b577c49c..6c8a0e56a6 100644 --- a/packages/frontend/src/components/MkPagePreview.vue +++ b/packages/frontend/src/components/MkPagePreview.vue @@ -114,7 +114,6 @@ const props = defineProps<{ & + article { left: 0; - width: 100%; } } } @@ -124,6 +123,7 @@ const props = defineProps<{ > .thumbnail { height: 80px; + overflow: clip; } > article { diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 163cba5e3c..d1d4c2106c 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ComputedRef, onMounted, onUnmounted, provide, shallowRef } from 'vue'; +import { ComputedRef, onMounted, onUnmounted, provide, shallowRef, ref, computed } from 'vue'; import RouterView from '@/components/global/RouterView.vue'; import MkWindow from '@/components/MkWindow.vue'; import { popout as _popout } from '@/scripts/popout.js'; @@ -55,16 +55,16 @@ defineEmits<{ const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue'))); const contents = shallowRef<HTMLElement>(); -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); -let windowEl = $shallowRef<InstanceType<typeof MkWindow>>(); -const history = $ref<{ path: string; key: any; }[]>([{ +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(), }]); -const buttonsLeft = $computed(() => { +const buttonsLeft = computed(() => { const buttons = []; - if (history.length > 1) { + if (history.value.length > 1) { buttons.push({ icon: 'ph-arrow-left ph-bold ph-lg', onClick: back, @@ -73,7 +73,7 @@ const buttonsLeft = $computed(() => { return buttons; }); -const buttonsRight = $computed(() => { +const buttonsRight = computed(() => { const buttons = [{ icon: 'ph-arrow-clockwise ph-bold ph-lg', title: i18n.ts.reload, @@ -86,22 +86,22 @@ const buttonsRight = $computed(() => { return buttons; }); -let reloadCount = $ref(0); +const reloadCount = ref(0); router.addListener('push', ctx => { - history.push({ path: ctx.path, key: ctx.key }); + history.value.push({ path: ctx.path, key: ctx.key }); }); provide('router', router); provideMetadataReceiver((info) => { - pageMetadata = info; + pageMetadata.value = info; }); provide('shouldOmitHeaderTitle', true); provide('shouldHeaderThin', true); provide('forceSpacerMin', true); provide('shouldBackButton', false); -const contextmenu = $computed(() => ([{ +const contextmenu = computed(() => ([{ icon: 'ph-eject ph-bold ph-lg', text: i18n.ts.showInPage, action: expand, @@ -113,8 +113,8 @@ const contextmenu = $computed(() => ([{ icon: 'ph-arrow-square-out ph-bold ph-lg', text: i18n.ts.openInNewTab, action: () => { - window.open(url + router.getCurrentPath(), '_blank'); - windowEl.close(); + window.open(url + router.getCurrentPath(), '_blank', 'noopener'); + windowEl.value.close(); }, }, { icon: 'ph-link ph-bold ph-lg', @@ -125,26 +125,26 @@ const contextmenu = $computed(() => ([{ }])); function back() { - history.pop(); - router.replace(history.at(-1)!.path, history.at(-1)!.key); + history.value.pop(); + router.replace(history.value.at(-1)!.path, history.value.at(-1)!.key); } function reload() { - reloadCount++; + reloadCount.value++; } function close() { - windowEl.close(); + windowEl.value.close(); } function expand() { mainRouter.push(router.getCurrentPath(), 'forcePage'); - windowEl.close(); + windowEl.value.close(); } function popout() { - _popout(router.getCurrentPath(), windowEl.$el); - windowEl.close(); + _popout(router.getCurrentPath(), windowEl.value.$el); + windowEl.value.close(); } useScrollPositionManager(() => getScrollContainer(contents.value), router); diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index e7796dfcb5..07347eda29 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts"> -import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, watch } from 'vue'; +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 { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js'; @@ -105,12 +105,12 @@ const emit = defineEmits<{ (ev: 'status', error: boolean): void; }>(); -let rootEl = $shallowRef<HTMLElement>(); +const rootEl = shallowRef<HTMLElement>(); // 遡り中かどうか -let backed = $ref(false); +const backed = ref(false); -let scrollRemove = $ref<(() => void) | null>(null); +const scrollRemove = ref<(() => void) | null>(null); /** * 表示するアイテムのソース @@ -142,8 +142,8 @@ const { enableInfiniteScroll, } = defaultStore.reactiveState; -const contentEl = $computed(() => props.pagination.pageEl ?? rootEl); -const scrollableElement = $computed(() => contentEl ? getScrollContainer(contentEl) : document.body); +const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value); +const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : document.body); const visibility = useDocumentVisibility(); @@ -153,40 +153,40 @@ const BACKGROUND_PAUSE_WAIT_SEC = 10; // 先頭が表示されているかどうかを検出 // https://qiita.com/mkataigi/items/0154aefd2223ce23398e -let scrollObserver = $ref<IntersectionObserver>(); +const scrollObserver = ref<IntersectionObserver>(); -watch([() => props.pagination.reversed, $$(scrollableElement)], () => { - if (scrollObserver) scrollObserver.disconnect(); +watch([() => props.pagination.reversed, scrollableElement], () => { + if (scrollObserver.value) scrollObserver.value.disconnect(); - scrollObserver = new IntersectionObserver(entries => { - backed = entries[0].isIntersecting; + scrollObserver.value = new IntersectionObserver(entries => { + backed.value = entries[0].isIntersecting; }, { - root: scrollableElement, + root: scrollableElement.value, rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px', threshold: 0.01, }); }, { immediate: true }); -watch($$(rootEl), () => { - scrollObserver?.disconnect(); +watch(rootEl, () => { + scrollObserver.value?.disconnect(); nextTick(() => { - if (rootEl) scrollObserver?.observe(rootEl); + if (rootEl.value) scrollObserver.value?.observe(rootEl.value); }); }); -watch([$$(backed), $$(contentEl)], () => { - if (!backed) { - if (!contentEl) return; +watch([backed, contentEl], () => { + if (!backed.value) { + if (!contentEl.value) return; - scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, executeQueue, TOLERANCE); + scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE); } else { - if (scrollRemove) scrollRemove(); - scrollRemove = null; + if (scrollRemove.value) scrollRemove.value(); + scrollRemove.value = null; } }); // パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど) -watch(() => props.pagination.params, init, { deep: true }); +watch(() => [props.pagination.endpoint, props.pagination.params], init, { deep: true }); watch(queue, (a, b) => { if (a.size === 0 && b.size === 0) return; @@ -206,6 +206,7 @@ async function init(): Promise<void> { await os.api(props.pagination.endpoint, { ...params, limit: props.pagination.limit ?? 10, + allowPartial: true, }).then(res => { for (let i = 0; i < res.length; i++) { const item = res[i]; @@ -253,14 +254,14 @@ const fetchMore = async (): Promise<void> => { } const reverseConcat = _res => { - const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight(); - const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY; + const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight(); + const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY; items.value = concatMapWithArray(items.value, _res); return nextTick(() => { - if (scrollableElement) { - scroll(scrollableElement, { top: oldScroll + (scrollableElement.scrollHeight - oldHeight), behavior: 'instant' }); + if (scrollableElement.value) { + scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); } else { window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); } @@ -350,7 +351,7 @@ const appearFetchMoreAhead = async (): Promise<void> => { fetchMoreAppearTimeout(); }; -const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl!, TOLERANCE); +const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE); watch(visibility, () => { if (visibility.value === 'hidden') { @@ -444,11 +445,11 @@ onActivated(() => { }); onDeactivated(() => { - isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl ? rootEl.scrollHeight - window.innerHeight : 0) : window.scrollY === 0; + isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl.value ? rootEl.value.scrollHeight - window.innerHeight : 0) : window.scrollY === 0; }); function toBottom() { - scrollToBottom(contentEl!); + scrollToBottom(contentEl.value!); } onBeforeMount(() => { @@ -476,13 +477,13 @@ onBeforeUnmount(() => { clearTimeout(preventAppearFetchMoreTimer.value); preventAppearFetchMoreTimer.value = null; } - scrollObserver?.disconnect(); + scrollObserver.value?.disconnect(); }); defineExpose({ items, queue, - backed, + backed: backed.value, more, reload, prepend, diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue index 3f244c42fd..711c54c7f1 100644 --- a/packages/frontend/src/components/MkPasswordDialog.vue +++ b/packages/frontend/src/components/MkPasswordDialog.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, shallowRef, ref } from 'vue'; import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; @@ -49,22 +49,22 @@ const emit = defineEmits<{ (ev: 'cancelled'): void; }>(); -const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>(); -const passwordInput = $shallowRef<InstanceType<typeof MkInput>>(); -const password = $ref(''); -const token = $ref(null); +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const passwordInput = shallowRef<InstanceType<typeof MkInput>>(); +const password = ref(''); +const token = ref(null); function onClose() { emit('cancelled'); - if (dialog) dialog.close(); + if (dialog.value) dialog.value.close(); } function done(res) { - emit('done', { password, token }); - if (dialog) dialog.close(); + emit('done', { password: password.value, token: token.value }); + if (dialog.value) dialog.value.close(); } onMounted(() => { - if (passwordInput) passwordInput.focus(); + if (passwordInput.value) passwordInput.value.focus(); }); </script> diff --git a/packages/frontend/src/components/MkPlusOneEffect.vue b/packages/frontend/src/components/MkPlusOneEffect.vue index 0bc98f4334..a741a3f7a8 100644 --- a/packages/frontend/src/components/MkPlusOneEffect.vue +++ b/packages/frontend/src/components/MkPlusOneEffect.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; import * as os from '@/os.js'; const props = withDefaults(defineProps<{ @@ -23,13 +23,13 @@ const emit = defineEmits<{ (ev: 'end'): void; }>(); -let up = $ref(false); +const up = ref(false); const zIndex = os.claimZIndex('middle'); const angle = (45 - (Math.random() * 90)) + 'deg'; onMounted(() => { window.setTimeout(() => { - up = true; + up.value = true; }, 10); window.setTimeout(() => { diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue index 146b9d7ccf..1d92374f4f 100644 --- a/packages/frontend/src/components/MkPopupMenu.vue +++ b/packages/frontend/src/components/MkPopupMenu.vue @@ -10,10 +10,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { ref, shallowRef } from 'vue'; import MkModal from './MkModal.vue'; import MkMenu from './MkMenu.vue'; -import { MenuItem } from '@/types/menu'; +import { MenuItem } from '@/types/menu.js'; defineProps<{ items: MenuItem[]; @@ -28,7 +28,7 @@ const emit = defineEmits<{ (ev: 'closing'): void; }>(); -let modal = $shallowRef<InstanceType<typeof MkModal>>(); +const modal = shallowRef<InstanceType<typeof MkModal>>(); const manualShowing = ref(true); const hiding = ref(false); @@ -60,14 +60,14 @@ function hide() { hiding.value = true; // closeは呼ぶ必要がある - modal?.close(); + modal.value?.close(); } function close() { manualShowing.value = false; // closeは呼ぶ必要がある - modal?.close(); + modal.value?.close(); } </script> diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 5c9ac40427..c9784fc40f 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -67,13 +67,14 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> <input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> <div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> - <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> + <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div> + <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> </div> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> <XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/> <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> - <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :user="postAccount ?? $i"/> + <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/> <div v-if="showingOptions" style="padding: 8px 16px;"> </div> <footer :class="$style.footer"> @@ -99,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide } from 'vue'; +import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue'; import * as mfm from '@sharkey/sfm-js'; import * as Misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; @@ -125,6 +126,7 @@ import { deepClone } from '@/scripts/clone.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement } from '@/scripts/achievements.js'; +import { emojiPicker } from '@/scripts/emoji-picker.js'; const modal = inject('modal'); @@ -135,6 +137,7 @@ const props = withDefaults(defineProps<{ mention?: Misskey.entities.User; specified?: Misskey.entities.User; initialText?: string; + initialCw?: string; initialVisibility?: (typeof Misskey.noteVisibilities)[number]; initialFiles?: Misskey.entities.DriveFile[]; initialLocalOnly?: boolean; @@ -144,7 +147,7 @@ const props = withDefaults(defineProps<{ fixed?: boolean; autofocus?: boolean; freezeAfterPosted?: boolean; - editId?: Misskey.entities.Note["id"]; + editId?: Misskey.entities.Note['id']; mock?: boolean; }>(), { initialVisibleUsers: () => [], @@ -163,41 +166,42 @@ const emit = defineEmits<{ (ev: 'fileChangeSensitive', fileId: string, to: boolean): void; }>(); -const textareaEl = $shallowRef<HTMLTextAreaElement | null>(null); -const cwInputEl = $shallowRef<HTMLInputElement | null>(null); -const hashtagsInputEl = $shallowRef<HTMLInputElement | null>(null); -const visibilityButton = $shallowRef<HTMLElement | null>(null); +const textareaEl = shallowRef<HTMLTextAreaElement | null>(null); +const cwInputEl = shallowRef<HTMLInputElement | null>(null); +const hashtagsInputEl = shallowRef<HTMLInputElement | null>(null); +const visibilityButton = shallowRef<HTMLElement | null>(null); -let posting = $ref(false); -let posted = $ref(false); -let text = $ref(props.initialText ?? ''); -let files = $ref(props.initialFiles ?? []); -let poll = $ref<{ +const posting = ref(false); +const posted = ref(false); +const text = ref(props.initialText ?? ''); +const files = ref(props.initialFiles ?? []); +const poll = ref<{ choices: string[]; multiple: boolean; expiresAt: string | null; expiredAfter: string | null; } | null>(null); -let useCw = $ref(false); -let showPreview = $ref(defaultStore.state.showPreview); -watch($$(showPreview), () => defaultStore.set('showPreview', showPreview)); -let cw = $ref<string | null>(null); -let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); -let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]); -let visibleUsers = $ref([]); +const useCw = ref<boolean>(!!props.initialCw); +const showPreview = ref(defaultStore.state.showPreview); +watch(showPreview, () => defaultStore.set('showPreview', showPreview.value)); +const cw = ref<string | null>(props.initialCw ?? null); +const localOnly = ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); +const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]); +const visibleUsers = ref([]); if (props.initialVisibleUsers) { props.initialVisibleUsers.forEach(pushVisibleUser); } -let reactionAcceptance = $ref(defaultStore.state.reactionAcceptance); -let autocomplete = $ref(null); -let draghover = $ref(false); -let quoteId = $ref(null); -let hasNotSpecifiedMentions = $ref(false); -let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]')); -let imeText = $ref(''); -let showingOptions = $ref(false); +const reactionAcceptance = ref(defaultStore.state.reactionAcceptance); +const autocomplete = ref(null); +const draghover = ref(false); +const quoteId = ref(null); +const hasNotSpecifiedMentions = ref(false); +const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]')); +const imeText = ref(''); +const showingOptions = ref(false); +const textAreaReadOnly = ref(false); -const draftKey = $computed((): string => { +const draftKey = computed((): string => { let key = props.channel ? `channel:${props.channel.id}` : ''; if (props.renote) { @@ -211,7 +215,7 @@ const draftKey = $computed((): string => { return key; }); -const placeholder = $computed((): string => { +const placeholder = computed((): string => { if (props.renote) { return i18n.ts._postForm.quotePlaceholder; } else if (props.reply) { @@ -231,7 +235,7 @@ const placeholder = $computed((): string => { } }); -const submitText = $computed((): string => { +const submitText = computed((): string => { return props.renote ? i18n.ts.quote : props.reply @@ -239,45 +243,45 @@ const submitText = $computed((): string => { : i18n.ts.note; }); -const textLength = $computed((): number => { - return (text + imeText).trim().length; +const textLength = computed((): number => { + return (text.value + imeText.value).trim().length; }); -const maxTextLength = $computed((): number => { +const maxTextLength = computed((): number => { return instance ? instance.maxNoteTextLength : 1000; }); -const canPost = $computed((): boolean => { - return !props.mock && !posting && !posted && - (1 <= textLength || 1 <= files.length || !!poll || !!props.renote) && - (textLength <= maxTextLength) && - (!poll || poll.choices.length >= 2); +const canPost = computed((): boolean => { + return !props.mock && !posting.value && !posted.value && + (1 <= textLength.value || 1 <= files.value.length || !!poll.value || !!props.renote) && + (textLength.value <= maxTextLength.value) && + (!poll.value || poll.value.choices.length >= 2); }); -const withHashtags = $computed(defaultStore.makeGetterSetter('postFormWithHashtags')); -const hashtags = $computed(defaultStore.makeGetterSetter('postFormHashtags')); +const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags')); +const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags')); -watch($$(text), () => { +watch(text, () => { checkMissingMention(); }, { immediate: true }); -watch($$(visibility), () => { +watch(visibility, () => { checkMissingMention(); }, { immediate: true }); -watch($$(visibleUsers), () => { +watch(visibleUsers, () => { checkMissingMention(); }, { deep: true, }); if (props.mention) { - text = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`; - text += ' '; + text.value = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`; + text.value += ' '; } if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) { - text = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `; + text.value = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `; } if (props.reply && props.reply.text != null) { @@ -295,32 +299,32 @@ if (props.reply && props.reply.text != null) { if ($i.username === x.username && (x.host == null || x.host === host)) continue; // 重複は除外 - if (text.includes(`${mention} `)) continue; + if (text.value.includes(`${mention} `)) continue; - text += `${mention} `; + text.value += `${mention} `; } } -if ($i?.isSilenced && visibility === 'public') { - visibility = 'home'; +if ($i?.isSilenced && visibility.value === 'public') { + visibility.value = 'home'; } if (props.channel) { - visibility = 'public'; - localOnly = true; // TODO: チャンネルが連合するようになった折には消す + visibility.value = 'public'; + localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す } // 公開以外へのリプライ時は元の公開範囲を引き継ぐ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) { - if (props.reply.visibility === 'home' && visibility === 'followers') { - visibility = 'followers'; - } else if (['home', 'followers'].includes(props.reply.visibility) && visibility === 'specified') { - visibility = 'specified'; + if (props.reply.visibility === 'home' && visibility.value === 'followers') { + visibility.value = 'followers'; + } else if (['home', 'followers'].includes(props.reply.visibility) && visibility.value === 'specified') { + visibility.value = 'specified'; } else { - visibility = props.reply.visibility; + visibility.value = props.reply.visibility; } - if (visibility === 'specified') { + if (visibility.value === 'specified') { if (props.reply.visibleUserIds) { os.api('users/show', { userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId), @@ -338,24 +342,24 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib } if (props.specified) { - visibility = 'specified'; + visibility.value = 'specified'; pushVisibleUser(props.specified); } // keep cw when reply if (defaultStore.state.keepCw && props.reply && props.reply.cw) { - useCw = true; - cw = props.reply.cw; + useCw.value = true; + cw.value = props.reply.cw; } function watchForDraft() { - watch($$(text), () => saveDraft()); - watch($$(useCw), () => saveDraft()); - watch($$(cw), () => saveDraft()); - watch($$(poll), () => saveDraft()); - watch($$(files), () => saveDraft(), { deep: true }); - watch($$(visibility), () => saveDraft()); - watch($$(localOnly), () => saveDraft()); + watch(text, () => saveDraft()); + watch(useCw, () => saveDraft()); + watch(cw, () => saveDraft()); + watch(poll, () => saveDraft()); + watch(files, () => saveDraft(), { deep: true }); + watch(visibility, () => saveDraft()); + watch(localOnly, () => saveDraft()); } function MFMWindow() { @@ -363,36 +367,36 @@ function MFMWindow() { } function checkMissingMention() { - if (visibility === 'specified') { - const ast = mfm.parse(text); + if (visibility.value === 'specified') { + const ast = mfm.parse(text.value); for (const x of extractMentions(ast)) { - if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) { - hasNotSpecifiedMentions = true; + if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) { + hasNotSpecifiedMentions.value = true; return; } } - hasNotSpecifiedMentions = false; } + hasNotSpecifiedMentions.value = false; } function addMissingMention() { - const ast = mfm.parse(text); + const ast = mfm.parse(text.value); for (const x of extractMentions(ast)) { - if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) { + 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 => { - visibleUsers.push(user); + visibleUsers.value.push(user); }); } } } function togglePoll() { - if (poll) { - poll = null; + if (poll.value) { + poll.value = null; } else { - poll = { + poll.value = { choices: ['', ''], multiple: false, expiresAt: null, @@ -402,13 +406,13 @@ function togglePoll() { } function addTag(tag: string) { - insertTextAtCursor(textareaEl, ` #${tag} `); + insertTextAtCursor(textareaEl.value, ` #${tag} `); } function focus() { - if (textareaEl) { - textareaEl.focus(); - textareaEl.setSelectionRange(textareaEl.value.length, textareaEl.value.length); + if (textareaEl.value) { + textareaEl.value.focus(); + textareaEl.value.setSelectionRange(textareaEl.value.value.length, textareaEl.value.value.length); } } @@ -417,55 +421,55 @@ function chooseFileFrom(ev) { selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => { for (const file of files_) { - files.push(file); + files.value.push(file); } }); } function detachFile(id) { - files = files.filter(x => x.id !== id); + files.value = files.value.filter(x => x.id !== id); } function updateFileSensitive(file, sensitive) { if (props.mock) { emit('fileChangeSensitive', file.id, sensitive); } - files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive; + files.value[files.value.findIndex(x => x.id === file.id)].isSensitive = sensitive; } function updateFileName(file, name) { - files[files.findIndex(x => x.id === file.id)].name = name; + files.value[files.value.findIndex(x => x.id === file.id)].name = name; } function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities.DriveFile): void { - files[files.findIndex(x => x.id === file.id)] = newFile; + files.value[files.value.findIndex(x => x.id === file.id)] = newFile; } function upload(file: File, name?: string): void { if (props.mock) return; uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { - files.push(res); + files.value.push(res); }); } function setVisibility() { if (props.channel) { - visibility = 'public'; - localOnly = true; // TODO: チャンネルが連合するようになった折には消す + visibility.value = 'public'; + localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す return; } os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { - currentVisibility: visibility, + currentVisibility: visibility.value, isSilenced: $i?.isSilenced, - localOnly: localOnly, - src: visibilityButton, + localOnly: localOnly.value, + src: visibilityButton.value, }, { changeVisibility: v => { - visibility = v; + visibility.value = v; if (defaultStore.state.rememberNoteVisibility) { - defaultStore.set('visibility', visibility); + defaultStore.set('visibility', visibility.value); } }, }, 'closed'); @@ -473,14 +477,14 @@ function setVisibility() { async function toggleLocalOnly() { if (props.channel) { - visibility = 'public'; - localOnly = true; // TODO: チャンネルが連合するようになった折には消す + visibility.value = 'public'; + localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す return; } const neverShowInfo = miLocalStorage.getItem('neverShowLocalOnlyInfo'); - if (!localOnly && neverShowInfo !== 'true') { + if (!localOnly.value && neverShowInfo !== 'true') { const confirm = await os.actions({ type: 'question', title: i18n.ts.disableFederationConfirm, @@ -510,7 +514,7 @@ async function toggleLocalOnly() { } } - localOnly = !localOnly; + localOnly.value = !localOnly.value; } async function toggleReactionAcceptance() { @@ -523,15 +527,15 @@ async function toggleReactionAcceptance() { { value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }, { value: 'likeOnly' as const, text: i18n.ts.likeOnly }, ], - default: reactionAcceptance, + default: reactionAcceptance.value, }); if (select.canceled) return; - reactionAcceptance = select.result; + reactionAcceptance.value = select.result; } function pushVisibleUser(user) { - if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) { - visibleUsers.push(user); + if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) { + visibleUsers.value.push(user); } } @@ -539,34 +543,34 @@ function addVisibleUser() { os.selectUser().then(user => { pushVisibleUser(user); - if (!text.toLowerCase().includes(`@${user.username.toLowerCase()}`)) { - text = `@${Misskey.acct.toString(user)} ${text}`; + if (!text.value.toLowerCase().includes(`@${user.username.toLowerCase()}`)) { + text.value = `@${Misskey.acct.toString(user)} ${text.value}`; } }); } function removeVisibleUser(user) { - visibleUsers = erase(user, visibleUsers); + visibleUsers.value = erase(user, visibleUsers.value); } function clear() { - text = ''; - files = []; - poll = null; - quoteId = null; + text.value = ''; + files.value = []; + poll.value = null; + quoteId.value = null; } function onKeydown(ev: KeyboardEvent) { - if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost) post(); + if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post(); if (ev.key === 'Escape') emit('esc'); } function onCompositionUpdate(ev: CompositionEvent) { - imeText = ev.data; + imeText.value = ev.data; } function onCompositionEnd(ev: CompositionEvent) { - imeText = ''; + imeText.value = ''; } async function onPaste(ev: ClipboardEvent) { @@ -584,7 +588,7 @@ async function onPaste(ev: ClipboardEvent) { const paste = ev.clipboardData.getData('text'); - if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) { + if (!props.renote && !quoteId.value && paste.startsWith(url + '/notes/')) { ev.preventDefault(); os.confirm({ @@ -592,11 +596,11 @@ async function onPaste(ev: ClipboardEvent) { text: i18n.ts.quoteQuestion, }).then(({ canceled }) => { if (canceled) { - insertTextAtCursor(textareaEl, paste); + insertTextAtCursor(textareaEl.value, paste); return; } - quoteId = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; + quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; }); } } @@ -607,7 +611,7 @@ function onDragover(ev) { const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; if (isFile || isDriveFile) { ev.preventDefault(); - draghover = true; + draghover.value = true; switch (ev.dataTransfer.effectAllowed) { case 'all': case 'uninitialized': @@ -628,15 +632,15 @@ function onDragover(ev) { } function onDragenter(ev) { - draghover = true; + draghover.value = true; } function onDragleave(ev) { - draghover = false; + draghover.value = false; } function onDrop(ev): void { - draghover = false; + draghover.value = false; // ファイルだったら if (ev.dataTransfer.files.length > 0) { @@ -649,7 +653,7 @@ function onDrop(ev): void { const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); - files.push(file); + files.value.push(file); ev.preventDefault(); } //#endregion @@ -660,16 +664,16 @@ function saveDraft() { const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); - draftData[draftKey] = { + draftData[draftKey.value] = { updatedAt: new Date(), data: { - text: text, - useCw: useCw, - cw: cw, - visibility: visibility, - localOnly: localOnly, - files: files, - poll: poll, + text: text.value, + useCw: useCw.value, + cw: cw.value, + visibility: visibility.value, + localOnly: localOnly.value, + files: files.value, + poll: poll.value, }, }; @@ -679,13 +683,13 @@ function saveDraft() { function deleteDraft() { const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); - delete draftData[draftKey]; + delete draftData[draftKey.value]; miLocalStorage.setItem('drafts', JSON.stringify(draftData)); } async function post(ev?: MouseEvent) { - if (useCw && (cw == null || cw.trim() === '')) { + if (useCw.value && (cw.value == null || cw.value.trim() === '')) { os.alert({ type: 'error', text: i18n.ts.cwNotationRequired, @@ -704,13 +708,13 @@ async function post(ev?: MouseEvent) { if (props.mock) return; const annoying = - text.includes('$[x2') || - text.includes('$[x3') || - text.includes('$[x4') || - text.includes('$[scale') || - text.includes('$[position'); + text.value.includes('$[x2') || + text.value.includes('$[x3') || + text.value.includes('$[x4') || + text.value.includes('$[scale') || + text.value.includes('$[position'); - if (annoying && visibility === 'public') { + if (annoying && visibility.value === 'public') { const { canceled, result } = await os.actions({ type: 'warning', text: i18n.ts.thisPostMayBeAnnoying, @@ -730,27 +734,27 @@ async function post(ev?: MouseEvent) { if (canceled) return; if (result === 'cancel') return; if (result === 'home') { - visibility = 'home'; + visibility.value = 'home'; } } let postData = { - text: text === '' ? null : text, - fileIds: files.length > 0 ? files.map(f => f.id) : undefined, + text: text.value === '' ? null : text.value, + fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined, replyId: props.reply ? props.reply.id : undefined, - renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined, + renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined, channelId: props.channel ? props.channel.id : undefined, - poll: poll, - cw: useCw ? cw ?? '' : null, - localOnly: localOnly, - visibility: visibility, - visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined, - reactionAcceptance, + poll: poll.value, + cw: useCw.value ? cw.value ?? '' : null, + localOnly: localOnly.value, + visibility: visibility.value, + visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined, + reactionAcceptance: reactionAcceptance.value, editId: props.editId ? props.editId : undefined, }; - if (withHashtags && hashtags && hashtags.trim() !== '') { - const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); + 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_; } @@ -767,15 +771,15 @@ async function post(ev?: MouseEvent) { let token = undefined; - if (postAccount) { + if (postAccount.value) { const storedAccounts = await getAccounts(); - token = storedAccounts.find(x => x.id === postAccount.id)?.token; + token = storedAccounts.find(x => x.id === postAccount.value.id)?.token; } - posting = true; - os.api(postData.editId ? "notes/edit" : "notes/create", postData, token).then(() => { + posting.value = true; + os.api(postData.editId ? 'notes/edit' : 'notes/create', postData, token).then(() => { if (props.freezeAfterPosted) { - posted = true; + posted.value = true; } else { clear(); } @@ -787,8 +791,8 @@ async function post(ev?: MouseEvent) { const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[]; miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history)))); } - posting = false; - postAccount = null; + posting.value = false; + postAccount.value = null; incNotesCount(); if (notesCount === 1) { @@ -833,7 +837,7 @@ async function post(ev?: MouseEvent) { } }); }).catch(err => { - posting = false; + posting.value = false; os.alert({ type: 'error', text: err.message + '\n' + (err as any).id, @@ -847,12 +851,23 @@ function cancel() { function insertMention() { os.selectUser().then(user => { - insertTextAtCursor(textareaEl, '@' + Misskey.acct.toString(user) + ' '); + insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' '); }); } async function insertEmoji(ev: MouseEvent) { - os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl); + textAreaReadOnly.value = true; + + emojiPicker.show( + ev.currentTarget ?? ev.target, + emoji => { + insertTextAtCursor(textareaEl.value, emoji); + }, + () => { + textAreaReadOnly.value = false; + nextTick(() => focus()); + }, + ); } function showActions(ev) { @@ -860,17 +875,17 @@ function showActions(ev) { text: action.title, action: () => { action.handler({ - text: text, - cw: cw, + text: text.value, + cw: cw.value, }, (key, value) => { - if (key === 'text') { text = value; } - if (key === 'cw') { useCw = value !== null; cw = value; } + if (key === 'text') { text.value = value; } + if (key === 'cw') { useCw.value = value !== null; cw.value = value; } }); }, })), ev.currentTarget ?? ev.target); } -let postAccount = $ref<Misskey.entities.UserDetailed | null>(null); +const postAccount = ref<Misskey.entities.UserDetailed | null>(null); function openAccountMenu(ev: MouseEvent) { if (props.mock) return; @@ -878,12 +893,12 @@ function openAccountMenu(ev: MouseEvent) { openAccountMenu_({ withExtraOperation: false, includeCurrentAccount: true, - active: postAccount != null ? postAccount.id : $i.id, + active: postAccount.value != null ? postAccount.value.id : $i.id, onChoose: (account) => { if (account.id === $i.id) { - postAccount = null; + postAccount.value = null; } else { - postAccount = account; + postAccount.value = account; } }, }, ev); @@ -899,23 +914,23 @@ onMounted(() => { } // TODO: detach when unmount - new Autocomplete(textareaEl, $$(text)); - new Autocomplete(cwInputEl, $$(cw)); - new Autocomplete(hashtagsInputEl, $$(hashtags)); + new Autocomplete(textareaEl.value, text); + new Autocomplete(cwInputEl.value, cw); + new Autocomplete(hashtagsInputEl.value, hashtags); nextTick(() => { // 書きかけの投稿を復元 if (!props.instant && !props.mention && !props.specified && !props.mock) { - const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey]; + const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey.value]; if (draft) { - text = draft.data.text; - useCw = draft.data.useCw; - cw = draft.data.cw; - visibility = draft.data.visibility; - localOnly = draft.data.localOnly; - files = (draft.data.files || []).filter(draftFile => draftFile); + text.value = draft.data.text; + useCw.value = draft.data.useCw; + cw.value = draft.data.cw; + visibility.value = draft.data.visibility; + localOnly.value = draft.data.localOnly; + files.value = (draft.data.files || []).filter(draftFile => draftFile); if (draft.data.poll) { - poll = draft.data.poll; + poll.value = draft.data.poll; } } } @@ -923,21 +938,21 @@ onMounted(() => { // 削除して編集 if (props.initialNote) { const init = props.initialNote; - text = init.text ? init.text : ''; - files = init.files; - cw = init.cw; - useCw = init.cw != null; + text.value = init.text ? init.text : ''; + files.value = init.files; + cw.value = init.cw; + useCw.value = init.cw != null; if (init.poll) { - poll = { + poll.value = { choices: init.poll.choices.map(x => x.text), multiple: init.poll.multiple, expiresAt: init.poll.expiresAt ? new Date(init.poll.expiresAt).getTime().toString() : null, expiredAfter: init.poll.expiredAfter ? new Date(init.poll.expiredAfter).getTime().toString() : null, }; } - visibility = init.visibility; - localOnly = init.localOnly; - quoteId = init.renote ? init.renote.id : null; + visibility.value = init.visibility; + localOnly.value = init.localOnly; + quoteId.value = init.renote ? init.renote.id : null; } nextTick(() => watchForDraft()); @@ -1031,6 +1046,16 @@ defineExpose({ } } +.colorBar { + position: absolute; + top: 0px; + left: 12px; + width: 5px; + height: 100% ; + border-radius: 999px; + pointer-events: none; +} + .submitInner { padding: 0 12px; line-height: 34px; @@ -1066,8 +1091,9 @@ defineExpose({ .visibility { overflow: clip; - text-overflow: ellipsis; - white-space: nowrap; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 210px; &:enabled { > .headerRightButtonText { @@ -1288,5 +1314,6 @@ defineExpose({ .headerRight { gap: 0; } + } </style> diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue index 25a8788a38..cd25077bfb 100644 --- a/packages/frontend/src/components/MkPostFormDialog.vue +++ b/packages/frontend/src/components/MkPostFormDialog.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkModal from '@/components/MkModal.vue'; import MkPostForm from '@/components/MkPostForm.vue'; @@ -22,6 +22,7 @@ const props = defineProps<{ mention?: Misskey.entities.User; specified?: Misskey.entities.User; initialText?: string; + initialCw?: string; initialVisibility?: typeof Misskey.noteVisibilities; initialFiles?: Misskey.entities.DriveFile[]; initialLocalOnly?: boolean; @@ -37,11 +38,11 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -let modal = $shallowRef<InstanceType<typeof MkModal>>(); -let form = $shallowRef<InstanceType<typeof MkPostForm>>(); +const modal = shallowRef<InstanceType<typeof MkModal>>(); +const form = shallowRef<InstanceType<typeof MkPostForm>>(); function onPosted() { - modal.close({ + modal.value.close({ useSendAnimation: true, }); } diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index 9f50f7ad5d..e963697997 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -23,8 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted, watch } from 'vue'; -import { deviceKind } from '@/scripts/device-kind.js'; +import { onMounted, onUnmounted, ref, shallowRef } from 'vue'; import { i18n } from '@/i18n.js'; import { getScrollContainer } from '@/scripts/scroll.js'; @@ -35,15 +34,15 @@ const RELEASE_TRANSITION_DURATION = 200; const PULL_BRAKE_BASE = 1.5; const PULL_BRAKE_FACTOR = 170; -let isPullStart = $ref(false); -let isPullEnd = $ref(false); -let isRefreshing = $ref(false); -let pullDistance = $ref(0); +const isPullStart = ref(false); +const isPullEnd = ref(false); +const isRefreshing = ref(false); +const pullDistance = ref(0); let supportPointerDesktop = false; let startScreenY: number | null = null; -const rootEl = $shallowRef<HTMLDivElement>(); +const rootEl = shallowRef<HTMLDivElement>(); let scrollEl: HTMLElement | null = null; let disabled = false; @@ -66,17 +65,17 @@ function getScreenY(event) { } function moveStart(event) { - if (!isPullStart && !isRefreshing && !disabled) { - isPullStart = true; + if (!isPullStart.value && !isRefreshing.value && !disabled) { + isPullStart.value = true; startScreenY = getScreenY(event); - pullDistance = 0; + pullDistance.value = 0; } } function moveBySystem(to: number): Promise<void> { return new Promise(r => { - const startHeight = pullDistance; - const overHeight = pullDistance - to; + const startHeight = pullDistance.value; + const overHeight = pullDistance.value - to; if (overHeight < 1) { r(); return; @@ -85,36 +84,36 @@ function moveBySystem(to: number): Promise<void> { let intervalId = setInterval(() => { const time = Date.now() - startTime; if (time > RELEASE_TRANSITION_DURATION) { - pullDistance = to; + pullDistance.value = to; clearInterval(intervalId); r(); return; } const nextHeight = startHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time; - if (pullDistance < nextHeight) return; - pullDistance = nextHeight; + if (pullDistance.value < nextHeight) return; + pullDistance.value = nextHeight; }, 1); }); } async function fixOverContent() { - if (pullDistance > FIRE_THRESHOLD) { + if (pullDistance.value > FIRE_THRESHOLD) { await moveBySystem(FIRE_THRESHOLD); } } async function closeContent() { - if (pullDistance > 0) { + if (pullDistance.value > 0) { await moveBySystem(0); } } function moveEnd() { - if (isPullStart && !isRefreshing) { + if (isPullStart.value && !isRefreshing.value) { startScreenY = null; - if (isPullEnd) { - isPullEnd = false; - isRefreshing = true; + if (isPullEnd.value) { + isPullEnd.value = false; + isRefreshing.value = true; fixOverContent().then(() => { emit('refresh'); props.refresher().then(() => { @@ -122,17 +121,17 @@ function moveEnd() { }); }); } else { - closeContent().then(() => isPullStart = false); + closeContent().then(() => isPullStart.value = false); } } } function moving(event: TouchEvent | PointerEvent) { - if (!isPullStart || isRefreshing || disabled) return; + if (!isPullStart.value || isRefreshing.value || disabled) return; - if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) { - pullDistance = 0; - isPullEnd = false; + if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value)) { + pullDistance.value = 0; + isPullEnd.value = false; moveEnd(); return; } @@ -143,13 +142,13 @@ function moving(event: TouchEvent | PointerEvent) { const moveScreenY = getScreenY(event); const moveHeight = moveScreenY - startScreenY!; - pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE); + pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE); - if (pullDistance > 0) { + if (pullDistance.value > 0) { if (event.cancelable) event.preventDefault(); } - isPullEnd = pullDistance >= FIRE_THRESHOLD; + isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD; } /** @@ -159,8 +158,8 @@ function moving(event: TouchEvent | PointerEvent) { */ function refreshFinished() { closeContent().then(() => { - isPullStart = false; - isRefreshing = false; + isPullStart.value = false; + isRefreshing.value = false; }); } @@ -182,26 +181,26 @@ function onScrollContainerScroll() { } function registerEventListenersForReadyToPull() { - if (rootEl == null) return; - rootEl.addEventListener('touchstart', moveStart, { passive: true }); - rootEl.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない + if (rootEl.value == null) return; + rootEl.value.addEventListener('touchstart', moveStart, { passive: true }); + rootEl.value.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない } function unregisterEventListenersForReadyToPull() { - if (rootEl == null) return; - rootEl.removeEventListener('touchstart', moveStart); - rootEl.removeEventListener('touchmove', moving); + if (rootEl.value == null) return; + rootEl.value.removeEventListener('touchstart', moveStart); + rootEl.value.removeEventListener('touchmove', moving); } onMounted(() => { - if (rootEl == null) return; + if (rootEl.value == null) return; - scrollEl = getScrollContainer(rootEl); + scrollEl = getScrollContainer(rootEl.value); if (scrollEl == null) return; scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true }); - rootEl.addEventListener('touchend', moveEnd, { passive: true }); + rootEl.value.addEventListener('touchend', moveEnd, { passive: true }); registerEventListenersForReadyToPull(); }); diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue index ba64775298..ebbd5e6cdc 100644 --- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue +++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue @@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script setup lang="ts"> +import { ref } from 'vue'; import { $i, getAccounts } from '@/account.js'; import MkButton from '@/components/MkButton.vue'; import { instance } from '@/instance.js'; @@ -62,26 +63,26 @@ defineProps<{ }>(); // ServiceWorker registration -let registration = $ref<ServiceWorkerRegistration | undefined>(); +const registration = ref<ServiceWorkerRegistration | undefined>(); // If this browser supports push notification -let supported = $ref(false); +const supported = ref(false); // If this browser has already subscribed to push notification -let pushSubscription = $ref<PushSubscription | null>(null); -let pushRegistrationInServer = $ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>(); +const pushSubscription = ref<PushSubscription | null>(null); +const pushRegistrationInServer = ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>(); function subscribe() { - if (!registration || !supported || !instance.swPublickey) return; + if (!registration.value || !supported.value || !instance.swPublickey) return; // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters - return promiseDialog(registration.pushManager.subscribe({ + return promiseDialog(registration.value.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(instance.swPublickey), }) .then(async subscription => { - pushSubscription = subscription; + pushSubscription.value = subscription; // Register - pushRegistrationInServer = await api('sw/register', { + pushRegistrationInServer.value = await api('sw/register', { endpoint: subscription.endpoint, auth: encode(subscription.getKey('auth')), publickey: encode(subscription.getKey('p256dh')), @@ -102,12 +103,12 @@ function subscribe() { } async function unsubscribe() { - if (!pushSubscription) return; + if (!pushSubscription.value) return; - const endpoint = pushSubscription.endpoint; + const endpoint = pushSubscription.value.endpoint; const accounts = await getAccounts(); - pushRegistrationInServer = undefined; + pushRegistrationInServer.value = undefined; if ($i && accounts.length >= 2) { apiWithDialog('sw/unregister', { @@ -115,11 +116,11 @@ async function unsubscribe() { endpoint, }); } else { - pushSubscription.unsubscribe(); + pushSubscription.value.unsubscribe(); apiWithDialog('sw/unregister', { endpoint, }); - pushSubscription = null; + pushSubscription.value = null; } } @@ -150,20 +151,20 @@ if (navigator.serviceWorker == null) { // TODO: よしなに? } else { navigator.serviceWorker.ready.then(async swr => { - registration = swr; + registration.value = swr; - pushSubscription = await registration.pushManager.getSubscription(); + pushSubscription.value = await registration.value.pushManager.getSubscription(); if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) { - supported = true; + supported.value = true; - if (pushSubscription) { + if (pushSubscription.value) { const res = await api('sw/show-registration', { - endpoint: pushSubscription.endpoint, + endpoint: pushSubscription.value.endpoint, }); if (res) { - pushRegistrationInServer = res; + pushRegistrationInServer.value = res; } } } @@ -171,6 +172,6 @@ if (navigator.serviceWorker == null) { } defineExpose({ - pushRegistrationInServer: $$(pushRegistrationInServer), + pushRegistrationInServer: pushRegistrationInServer, }); </script> diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue index f22774ef97..edb3abe5f7 100644 --- a/packages/frontend/src/components/MkRadio.vue +++ b/packages/frontend/src/components/MkRadio.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { computed } from 'vue'; const props = defineProps<{ modelValue: any; @@ -36,7 +36,7 @@ const emit = defineEmits<{ (ev: 'update:modelValue', value: any): void; }>(); -let checked = $computed(() => props.modelValue === props.value); +const checked = computed(() => props.modelValue === props.value); function toggle(): void { if (props.disabled) return; diff --git a/packages/frontend/src/components/MkReactionEffect.vue b/packages/frontend/src/components/MkReactionEffect.vue index 88e262d880..75eb91e7ad 100644 --- a/packages/frontend/src/components/MkReactionEffect.vue +++ b/packages/frontend/src/components/MkReactionEffect.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; import * as os from '@/os.js'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; @@ -27,13 +27,13 @@ const emit = defineEmits<{ (ev: 'end'): void; }>(); -let up = $ref(false); +const up = ref(false); const zIndex = os.claimZIndex('middle'); const angle = (90 - (Math.random() * 180)) + 'deg'; onMounted(() => { window.setTimeout(() => { - up = true; + up.value = true; }, 10); window.setTimeout(() => { diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 42b5243e94..e7901316a2 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]" @click="toggleReaction()" > - <MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]" @click="toggleReaction()"/> + <MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]" @click="toggleReaction()"/> <span :class="$style.count">{{ count }}</span> </button> </template> @@ -28,6 +28,7 @@ import MkReactionEffect from '@/components/MkReactionEffect.vue'; import { claimAchievement } from '@/scripts/achievements.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; +import * as sound from '@/scripts/sound.js'; const props = defineProps<{ reaction: string; @@ -59,6 +60,10 @@ async function toggleReaction() { }); if (confirm.canceled) return; + if (oldReaction !== props.reaction) { + sound.play('reaction'); + } + if (mock) { emit('reactionToggled', props.reaction, (props.count - 1)); return; @@ -75,6 +80,8 @@ async function toggleReaction() { } }); } else { + sound.play('reaction'); + if (mock) { emit('reactionToggled', props.reaction, (props.count + 1)); return; @@ -132,12 +139,14 @@ if (!mock) { <style lang="scss" module> .root { - display: inline-block; + display: inline-flex; height: 42px; margin: 2px; padding: 0 6px; font-size: 1.5em; border-radius: var(--radius-sm); + align-items: center; + justify-content: center; &.canToggle { background: var(--buttonBg); @@ -176,7 +185,7 @@ if (!mock) { &.reacted, &.reacted:hover { background: var(--accentedBg); color: var(--accent); - box-shadow: 0 0 0px 1px var(--accent) inset; + box-shadow: 0 0 0 1px var(--accent) inset; > .count { color: var(--accent); @@ -188,7 +197,7 @@ if (!mock) { } } -.icon { +.limitWidth { max-width: 150px; } diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index 13d022977e..d2a5c431fe 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { inject, watch } from 'vue'; +import { inject, watch, ref } from 'vue'; import XReaction from '@/components/MkReactionsViewer.reaction.vue'; import { defaultStore } from '@/store.js'; @@ -38,31 +38,31 @@ const emit = defineEmits<{ const initialReactions = new Set(Object.keys(props.note.reactions)); -let reactions = $ref<[string, number][]>([]); -let hasMoreReactions = $ref(false); +const reactions = ref<[string, number][]>([]); +const hasMoreReactions = ref(false); -if (props.note.myReaction && !Object.keys(reactions).includes(props.note.myReaction)) { - reactions[props.note.myReaction] = props.note.reactions[props.note.myReaction]; +if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) { + reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction]; } function onMockToggleReaction(emoji: string, count: number) { if (!mock) return; - const i = reactions.findIndex((item) => item[0] === emoji); + const i = reactions.value.findIndex((item) => item[0] === emoji); if (i < 0) return; - emit('mockUpdateMyReaction', emoji, (count - reactions[i][1])); + emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1])); } watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { let newReactions: [string, number][] = []; - hasMoreReactions = Object.keys(newSource).length > maxNumber; + hasMoreReactions.value = Object.keys(newSource).length > maxNumber; - for (let i = 0; i < reactions.length; i++) { - const reaction = reactions[i][0]; + for (let i = 0; i < reactions.value.length; i++) { + const reaction = reactions.value[i][0]; if (reaction in newSource && newSource[reaction] !== 0) { - reactions[i][1] = newSource[reaction]; - newReactions.push(reactions[i]); + reactions.value[i][1] = newSource[reaction]; + newReactions.push(reactions.value[i]); } } @@ -80,7 +80,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]); } - reactions = newReactions; + reactions.value = newReactions; }, { immediate: true, deep: true }); </script> diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue index 3dc9a94ae2..e69aa1be80 100644 --- a/packages/frontend/src/components/MkRetentionHeatmap.vue +++ b/packages/frontend/src/components/MkRetentionHeatmap.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, nextTick } from 'vue'; +import { onMounted, nextTick, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; @@ -23,11 +23,11 @@ import { initChart } from '@/scripts/init-chart.js'; initChart(); -const rootEl = $shallowRef<HTMLDivElement>(null); -const chartEl = $shallowRef<HTMLCanvasElement>(null); +const rootEl = shallowRef<HTMLDivElement>(null); +const chartEl = shallowRef<HTMLCanvasElement>(null); const now = new Date(); let chartInstance: Chart = null; -let fetching = $ref(true); +const fetching = ref(true); const { handler: externalTooltipHandler } = useChartTooltip({ position: 'middle', @@ -38,8 +38,8 @@ async function renderChart() { chartInstance.destroy(); } - const wide = rootEl.offsetWidth > 600; - const narrow = rootEl.offsetWidth < 400; + const wide = rootEl.value.offsetWidth > 600; + const narrow = rootEl.value.offsetWidth < 400; const maxDays = wide ? 10 : narrow ? 5 : 7; @@ -66,7 +66,7 @@ async function renderChart() { } } - fetching = false; + fetching.value = false; await nextTick(); @@ -83,7 +83,7 @@ async function renderChart() { const marginEachCell = 12; - chartInstance = new Chart(chartEl, { + chartInstance = new Chart(chartEl.value, { type: 'matrix', data: { datasets: [{ diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index a8718b98d6..08830fca7a 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; +import { defineAsyncComponent, ref } from 'vue'; import { toUnicode } from 'punycode/'; import * as Misskey from 'misskey-js'; import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; @@ -62,17 +62,17 @@ import * as os from '@/os.js'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; -let signing = $ref(false); -let user = $ref<Misskey.entities.UserDetailed | null>(null); -let username = $ref(''); -let password = $ref(''); -let token = $ref(''); -let host = $ref(toUnicode(configHost)); -let totpLogin = $ref(false); -let queryingKey = $ref(false); -let credentialRequest = $ref<CredentialRequestOptions | null>(null); -let hCaptchaResponse = $ref(null); -let reCaptchaResponse = $ref(null); +const signing = ref(false); +const user = ref<Misskey.entities.UserDetailed | null>(null); +const username = ref(''); +const password = ref(''); +const token = ref(''); +const host = ref(toUnicode(configHost)); +const totpLogin = ref(false); +const queryingKey = ref(false); +const credentialRequest = ref<CredentialRequestOptions | null>(null); +const hCaptchaResponse = ref(null); +const reCaptchaResponse = ref(null); const emit = defineEmits<{ (ev: 'login', v: any): void; @@ -98,11 +98,11 @@ const props = defineProps({ function onUsernameChange(): void { os.api('users/show', { - username: username, + username: username.value, }).then(userResponse => { - user = userResponse; + user.value = userResponse; }, () => { - user = null; + user.value = null; }); } @@ -113,21 +113,21 @@ function onLogin(res: any): Promise<void> | void { } async function queryKey(): Promise<void> { - queryingKey = true; - await webAuthnRequest(credentialRequest) + queryingKey.value = true; + await webAuthnRequest(credentialRequest.value) .catch(() => { - queryingKey = false; + queryingKey.value = false; return Promise.reject(null); }).then(credential => { - credentialRequest = null; - queryingKey = false; - signing = true; + credentialRequest.value = null; + queryingKey.value = false; + signing.value = true; return os.api('signin', { - username, - password, + username: username.value, + password: password.value, credential: credential.toJSON(), - 'hcaptcha-response': hCaptchaResponse, - 'g-recaptcha-response': reCaptchaResponse, + 'hcaptcha-response': hCaptchaResponse.value, + 'g-recaptcha-response': reCaptchaResponse.value, }); }).then(res => { emit('login', res); @@ -138,39 +138,39 @@ async function queryKey(): Promise<void> { type: 'error', text: i18n.ts.signinFailed, }); - signing = false; + signing.value = false; }); } function onSubmit(): void { - signing = true; - if (!totpLogin && user && user.twoFactorEnabled) { - if (webAuthnSupported() && user.securityKeys) { + signing.value = true; + if (!totpLogin.value && user.value && user.value.twoFactorEnabled) { + if (webAuthnSupported() && user.value.securityKeys) { os.api('signin', { - username, - password, - 'hcaptcha-response': hCaptchaResponse, - 'g-recaptcha-response': reCaptchaResponse, + username: username.value, + password: password.value, + 'hcaptcha-response': hCaptchaResponse.value, + 'g-recaptcha-response': reCaptchaResponse.value, }).then(res => { - totpLogin = true; - signing = false; - credentialRequest = parseRequestOptionsFromJSON({ + totpLogin.value = true; + signing.value = false; + credentialRequest.value = parseRequestOptionsFromJSON({ publicKey: res, }); }) .then(() => queryKey()) .catch(loginFailed); } else { - totpLogin = true; - signing = false; + totpLogin.value = true; + signing.value = false; } } else { os.api('signin', { - username, - password, - 'hcaptcha-response': hCaptchaResponse, - 'g-recaptcha-response': reCaptchaResponse, - token: user?.twoFactorEnabled ? token : undefined, + username: username.value, + password: password.value, + 'hcaptcha-response': hCaptchaResponse.value, + 'g-recaptcha-response': reCaptchaResponse.value, + token: user.value?.twoFactorEnabled ? token.value : undefined, }).then(res => { emit('login', res); onLogin(res); @@ -218,8 +218,8 @@ function loginFailed(err: any): void { } } - totpLogin = false; - signing = false; + totpLogin.value = false; + signing.value = false; } function resetPassword(): void { diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index 05cef6ed3b..6f961cff05 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { shallowRef } from 'vue'; import MkSignin from '@/components/MkSignin.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; @@ -39,15 +39,15 @@ const emit = defineEmits<{ (ev: 'cancelled'): void; }>(); -const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); function onClose() { emit('cancelled'); - if (dialog) dialog.close(); + if (dialog.value) dialog.value.close(); } function onLogin(res) { emit('done', res); - if (dialog) dialog.close(); + if (dialog.value) dialog.value.close(); } </script> diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 389acb82bc..b46dc4bd93 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -80,11 +80,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import { toUnicode } from 'punycode/'; import MkButton from './MkButton.vue'; import MkInput from './MkInput.vue'; -import MkSwitch from './MkSwitch.vue'; import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; import * as config from '@/config.js'; import * as os from '@/os.js'; @@ -106,35 +105,35 @@ const emit = defineEmits<{ const host = toUnicode(config.host); -let hcaptcha = $ref<Captcha | undefined>(); -let recaptcha = $ref<Captcha | undefined>(); -let turnstile = $ref<Captcha | undefined>(); +const hcaptcha = ref<Captcha | undefined>(); +const recaptcha = ref<Captcha | undefined>(); +const turnstile = ref<Captcha | undefined>(); -let username: string = $ref(''); -let password: string = $ref(''); -let retypedPassword: string = $ref(''); -let invitationCode: string = $ref(''); -let reason: string = $ref(''); -let email = $ref(''); -let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null); -let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null); -let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref(''); -let passwordRetypeState: null | 'match' | 'not-match' = $ref(null); -let submitting: boolean = $ref(false); -let hCaptchaResponse = $ref(null); -let reCaptchaResponse = $ref(null); -let turnstileResponse = $ref(null); -let usernameAbortController: null | AbortController = $ref(null); -let emailAbortController: null | AbortController = $ref(null); +const username = ref<string>(''); +const password = ref<string>(''); +const retypedPassword = ref<string>(''); +const invitationCode = ref<string>(''); +const reason = ref<string>(''); +const email = ref(''); +const usernameState = ref<null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range'>(null); +const emailState = ref<null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error'>(null); +const passwordStrength = ref<'' | 'low' | 'medium' | 'high'>(''); +const passwordRetypeState = ref<null | 'match' | 'not-match'>(null); +const submitting = ref<boolean>(false); +const hCaptchaResponse = ref(null); +const reCaptchaResponse = ref(null); +const turnstileResponse = ref(null); +const usernameAbortController = ref<null | AbortController>(null); +const emailAbortController = ref<null | AbortController>(null); -const shouldDisableSubmitting = $computed((): boolean => { - return submitting || - instance.enableHcaptcha && !hCaptchaResponse || - instance.enableRecaptcha && !reCaptchaResponse || - instance.enableTurnstile && !turnstileResponse || - instance.emailRequiredForSignup && emailState !== 'ok' || - usernameState !== 'ok' || - passwordRetypeState !== 'match'; +const shouldDisableSubmitting = computed((): boolean => { + return submitting.value || + instance.enableHcaptcha && !hCaptchaResponse.value || + instance.enableRecaptcha && !reCaptchaResponse.value || + instance.enableTurnstile && !turnstileResponse.value || + instance.emailRequiredForSignup && emailState.value !== 'ok' || + usernameState.value !== 'ok' || + passwordRetypeState.value !== 'match'; }); function getPasswordStrength(source: string): number { @@ -162,57 +161,57 @@ function getPasswordStrength(source: string): number { } function onChangeUsername(): void { - if (username === '') { - usernameState = null; + if (username.value === '') { + usernameState.value = null; return; } { const err = - !username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' : - username.length < 1 ? 'min-range' : - username.length > 20 ? 'max-range' : + !username.value.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' : + username.value.length < 1 ? 'min-range' : + username.value.length > 20 ? 'max-range' : null; if (err) { - usernameState = err; + usernameState.value = err; return; } } - if (usernameAbortController != null) { - usernameAbortController.abort(); + if (usernameAbortController.value != null) { + usernameAbortController.value.abort(); } - usernameState = 'wait'; - usernameAbortController = new AbortController(); + usernameState.value = 'wait'; + usernameAbortController.value = new AbortController(); os.api('username/available', { - username, - }, undefined, usernameAbortController.signal).then(result => { - usernameState = result.available ? 'ok' : 'unavailable'; + username: username.value, + }, undefined, usernameAbortController.value.signal).then(result => { + usernameState.value = result.available ? 'ok' : 'unavailable'; }).catch((err) => { if (err.name !== 'AbortError') { - usernameState = 'error'; + usernameState.value = 'error'; } }); } function onChangeEmail(): void { - if (email === '') { - emailState = null; + if (email.value === '') { + emailState.value = null; return; } - if (emailAbortController != null) { - emailAbortController.abort(); + if (emailAbortController.value != null) { + emailAbortController.value.abort(); } - emailState = 'wait'; - emailAbortController = new AbortController(); + emailState.value = 'wait'; + emailAbortController.value = new AbortController(); os.api('email-address/available', { - emailAddress: email, - }, undefined, emailAbortController.signal).then(result => { - emailState = result.available ? 'ok' : + emailAddress: email.value, + }, undefined, emailAbortController.value.signal).then(result => { + emailState.value = result.available ? 'ok' : result.reason === 'used' ? 'unavailable:used' : result.reason === 'format' ? 'unavailable:format' : result.reason === 'disposable' ? 'unavailable:disposable' : @@ -221,50 +220,49 @@ function onChangeEmail(): void { 'unavailable'; }).catch((err) => { if (err.name !== 'AbortError') { - emailState = 'error'; + emailState.value = 'error'; } }); } function onChangePassword(): void { - if (password === '') { - passwordStrength = ''; + if (password.value === '') { + passwordStrength.value = ''; return; } - const strength = getPasswordStrength(password); - passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; + const strength = getPasswordStrength(password.value); + passwordStrength.value = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; } function onChangePasswordRetype(): void { - if (retypedPassword === '') { - passwordRetypeState = null; + if (retypedPassword.value === '') { + passwordRetypeState.value = null; return; } - passwordRetypeState = password === retypedPassword ? 'match' : 'not-match'; + passwordRetypeState.value = password.value === retypedPassword.value ? 'match' : 'not-match'; } async function onSubmit(): Promise<void> { - if (submitting) return; - submitting = true; + if (submitting.value) return; + submitting.value = true; try { await os.api('signup', { - username, - password, - emailAddress: email, - invitationCode, - reason, - 'hcaptcha-response': hCaptchaResponse, - 'g-recaptcha-response': reCaptchaResponse, - 'turnstile-response': turnstileResponse, + username: username.value, + password: password.value, + emailAddress: email.value, + invitationCode: invitationCode.value, + reason: reason.value, + 'hcaptcha-response': hCaptchaResponse.value, + 'g-recaptcha-response': reCaptchaResponse.value, }); if (instance.emailRequiredForSignup) { os.alert({ type: 'success', title: i18n.ts._signup.almostThere, - text: i18n.t('_signup.emailSent', { email }), + text: i18n.t('_signup.emailSent', { email: email.value }), }); emit('signupEmailPending'); } else if (instance.approvalRequiredForSignup) { @@ -276,8 +274,8 @@ async function onSubmit(): Promise<void> { emit('approvalPending'); } else { const res = await os.api('signin', { - username, - password, + username: username.value, + password: password.value, }); emit('signup', res); @@ -286,10 +284,10 @@ async function onSubmit(): Promise<void> { } } } catch { - submitting = false; - hcaptcha?.reset?.(); - recaptcha?.reset?.(); - turnstile?.reset?.(); + submitting.value = false; + hcaptcha.value?.reset?.(); + recaptcha.value?.reset?.(); + turnstile.value?.reset?.(); os.alert({ type: 'error', diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue index 09eac0732a..bc4fec305b 100644 --- a/packages/frontend/src/components/MkSignupDialog.rules.vue +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted, ref, watch } from 'vue'; +import { computed, ref } from 'vue'; import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; @@ -96,7 +96,7 @@ const tosPrivacyPolicyLabel = computed(() => { } else if (availablePrivacyPolicy) { return i18n.ts.privacyPolicy; } else { - return ""; + return ''; } }); diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue index 73d3b644e9..c8020c6636 100644 --- a/packages/frontend/src/components/MkSignupDialog.vue +++ b/packages/frontend/src/components/MkSignupDialog.vue @@ -33,13 +33,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; -import { $ref } from 'vue/macros'; +import { shallowRef, ref } from 'vue'; + import XSignup from '@/components/MkSignupDialog.form.vue'; import XServerRules from '@/components/MkSignupDialog.rules.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; const props = withDefaults(defineProps<{ autoSet?: boolean; @@ -52,17 +51,17 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); -const isAcceptedServerRule = $ref(false); +const isAcceptedServerRule = ref(false); function onSignup(res) { emit('done', res); - dialog.close(); + dialog.value.close(); } function onSignupEmailPending() { - dialog.close(); + dialog.value.close(); } function onApprovalPending() { diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index a91f1f444c..c071fb938a 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="{ [$style.clickToOpen]: defaultStore.state.clickToOpen }" @click="defaultStore.state.clickToOpen ? noteclick(note.id) : undefined"> <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> - <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" v-on:click.stop><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA> + <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" @click.stop><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA> <Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :isAnim="allowAnim" :emojiUrls="note.emojis"/> - <MkButton v-if="!allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> - <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> + <MkButton v-if="!allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> + <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> <div v-if="note.text && translating || note.text && translation" :class="$style.translation"> <MkLoading v-if="translating" mini/> <div v-else> @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :text="translation.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> </div> </div> - <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" v-on:click.stop>RN: ...</MkA> + <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" @click.stop>RN: ...</MkA> </div> <details v-if="note.files.length > 0" :open="!defaultStore.state.collapseFiles && !hideFiles"> <summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary> @@ -39,14 +39,13 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import * as mfm from '@sharkey/sfm-js'; import MkMediaList from '@/components/MkMediaList.vue'; import MkPoll from '@/components/MkPoll.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; import { shouldCollapsed } from '@/scripts/collapsed.js'; import { defaultStore } from '@/store.js'; import { useRouter } from '@/router.js'; @@ -69,25 +68,25 @@ function noteclick(id: string) { } } -const parsed = $computed(() => props.note.text ? mfm.parse(props.note.text) : null); -const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null); -let allowAnim = $ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); +const parsed = computed(() => props.note.text ? mfm.parse(props.note.text) : null); +const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null); +let allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); const isLong = defaultStore.state.expandLongNote && !props.hideFiles ? false : shouldCollapsed(props.note, []); function animatedMFM() { - if (allowAnim) { - allowAnim = false; + if (allowAnim.value) { + allowAnim.value = false; } else { os.confirm({ type: 'warning', text: i18n.ts._animatedMFM._alert.text, okText: i18n.ts._animatedMFM._alert.confirm, - }).then((res) => { if (!res.canceled) allowAnim = true; }); + }).then((res) => { if (!res.canceled) allowAnim.value = true; }); } } -const collapsed = $ref(isLong); +const collapsed = ref(isLong); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue index 7521bd6c76..35e5aebbdd 100644 --- a/packages/frontend/src/components/MkSwitch.vue +++ b/packages/frontend/src/components/MkSwitch.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"> +<div :class="[$style.root, { [$style.disabled]: disabled }]"> <input ref="input" type="checkbox" @@ -64,9 +64,6 @@ const toggle = () => { opacity: 0.6; cursor: not-allowed; } - - //&.checked { - //} } .input { diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue index a3d82fee5e..083c34906f 100644 --- a/packages/frontend/src/components/MkTagCloud.vue +++ b/packages/frontend/src/components/MkTagCloud.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, watch, onBeforeUnmount } from 'vue'; +import { onMounted, watch, onBeforeUnmount, ref, shallowRef } from 'vue'; import tinycolor from 'tinycolor2'; const loaded = !!window.TagCanvas; @@ -23,13 +23,13 @@ const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz'; const computedStyle = getComputedStyle(document.documentElement); const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join(''); const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join(''); -let available = $ref(false); -let rootEl = $shallowRef<HTMLElement | null>(null); -let canvasEl = $shallowRef<HTMLCanvasElement | null>(null); -let tagsEl = $shallowRef<HTMLElement | null>(null); -let width = $ref(300); +const available = ref(false); +const rootEl = shallowRef<HTMLElement | null>(null); +const canvasEl = shallowRef<HTMLCanvasElement | null>(null); +const tagsEl = shallowRef<HTMLElement | null>(null); +const width = ref(300); -watch($$(available), () => { +watch(available, () => { try { window.TagCanvas.Start(idForCanvas, idForTags, { textColour: '#ffffff', @@ -52,15 +52,15 @@ watch($$(available), () => { }); onMounted(() => { - width = rootEl.offsetWidth; + width.value = rootEl.value.offsetWidth; if (loaded) { - available = true; + available.value = true; } else { document.head.appendChild(Object.assign(document.createElement('script'), { async: true, src: '/client-assets/tagcanvas.min.js', - })).addEventListener('load', () => available = true); + })).addEventListener('load', () => available.value = true); } }); diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue index c35274959e..5c70adde11 100644 --- a/packages/frontend/src/components/MkTextarea.vue +++ b/packages/frontend/src/components/MkTextarea.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only :readonly="readonly" :placeholder="placeholder" :pattern="pattern" - :autocomplete="autocomplete" + :autocomplete="props.autocomplete" :spellcheck="spellcheck" @focus="focused = true" @blur="focused = false" @@ -26,16 +26,21 @@ SPDX-License-Identifier: AGPL-3.0-only ></textarea> </div> <div :class="$style.caption"><slot name="caption"></slot></div> + <button v-if="mfmPreview" style="font-size: 0.85em;" class="_textButton" type="button" @click="preview = !preview">{{ i18n.ts.preview }}</button> + <div v-if="mfmPreview" v-show="preview" v-panel :class="$style.mfmPreview"> + <Mfm :text="v"/> + </div> <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> </div> </template> <script lang="ts" setup> -import { onMounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue'; +import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue'; import { debounce } from 'throttle-debounce'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; +import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js'; const props = defineProps<{ modelValue: string | null; @@ -46,6 +51,8 @@ const props = defineProps<{ placeholder?: string; autofocus?: boolean; autocomplete?: string; + mfmAutocomplete?: boolean | SuggestionType[], + mfmPreview?: boolean; spellcheck?: boolean; debounce?: boolean; manualSave?: boolean; @@ -68,6 +75,8 @@ const changed = ref(false); const invalid = ref(false); const filled = computed(() => v.value !== '' && v.value != null); const inputEl = shallowRef<HTMLTextAreaElement>(); +const preview = ref(false); +let autocomplete: Autocomplete; const focus = () => inputEl.value.focus(); const onInput = (ev) => { @@ -82,6 +91,16 @@ const onKeydown = (ev: KeyboardEvent) => { if (ev.code === 'Enter') { emit('enter'); } + + if (props.code && ev.key === 'Tab') { + const pos = inputEl.value?.selectionStart ?? 0; + 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); + }); + ev.preventDefault(); + } }; const updated = () => { @@ -113,6 +132,16 @@ onMounted(() => { focus(); } }); + + if (props.mfmAutocomplete) { + autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete); + } +}); + +onUnmounted(() => { + if (autocomplete) { + autocomplete.detach(); + } }); </script> @@ -194,4 +223,12 @@ onMounted(() => { .save { margin: 8px 0 0 0; } + +.mfmPreview { + padding: 12px; + border-radius: var(--radius); + box-sizing: border-box; + min-height: 130px; + pointer-events: none; +} </style> diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 85096dc583..8bd68c0fd2 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, onUnmounted, provide } from 'vue'; +import { computed, watch, onUnmounted, provide, ref } from 'vue'; import { Connection } from 'misskey-js/built/streaming.js'; import MkNotes from '@/components/MkNotes.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; @@ -65,8 +65,8 @@ type TimelineQueryType = { roleId?: string } -const prComponent: InstanceType<typeof MkPullToRefresh> = $ref(); -const tlComponent: InstanceType<typeof MkNotes> = $ref(); +const prComponent = ref<InstanceType<typeof MkPullToRefresh>>(); +const tlComponent = ref<InstanceType<typeof MkNotes>>(); let tlNotesCount = 0; @@ -77,7 +77,7 @@ const prepend = note => { note._shouldInsertAd_ = true; } - tlComponent.pagingComponent?.prepend(note); + tlComponent.value.pagingComponent?.prepend(note); emit('note'); @@ -271,7 +271,7 @@ function reloadTimeline() { return new Promise<void>((res) => { tlNotesCount = 0; - tlComponent.pagingComponent?.reload().then(() => { + tlComponent.value.pagingComponent?.reload().then(() => { res(); }); }); diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue index 3b26b50a0b..82cd236193 100644 --- a/packages/frontend/src/components/MkToast.vue +++ b/packages/frontend/src/components/MkToast.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; @@ -35,11 +35,11 @@ const emit = defineEmits<{ }>(); const zIndex = os.claimZIndex('high'); -let showing = $ref(true); +const showing = ref(true); onMounted(() => { window.setTimeout(() => { - showing = false; + showing.value = false; }, 4000); }); </script> diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue index 8958accc4a..f5fa86a908 100644 --- a/packages/frontend/src/components/MkTokenGenerateWindow.vue +++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { shallowRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkInput from './MkInput.vue'; import MkSwitch from './MkSwitch.vue'; @@ -67,37 +67,37 @@ const emit = defineEmits<{ (ev: 'done', result: { name: string | null, permissions: string[] }): void; }>(); -const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>(); -let name = $ref(props.initialName); -let permissions = $ref({}); +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const name = ref(props.initialName); +const permissions = ref({}); if (props.initialPermissions) { for (const kind of props.initialPermissions) { - permissions[kind] = true; + permissions.value[kind] = true; } } else { for (const kind of Misskey.permissions) { - permissions[kind] = false; + permissions.value[kind] = false; } } function ok(): void { emit('done', { - name: name, - permissions: Object.keys(permissions).filter(p => permissions[p]), + name: name.value, + permissions: Object.keys(permissions.value).filter(p => permissions.value[p]), }); - dialog.close(); + dialog.value.close(); } function disableAll(): void { - for (const p in permissions) { - permissions[p] = false; + for (const p in permissions.value) { + permissions.value[p] = false; } } function enableAll(): void { - for (const p in permissions) { - permissions[p] = true; + for (const p in permissions.value) { + permissions.value[p] = true; } } </script> diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue index 421c0a8af8..c2384423fd 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.divider"></div> <I18n :src="i18n.ts._initialTutorial._timeline.description3" tag="div" style="padding: 0 16px;"> <template #link> - <a href="https://misskey-hub.net/docs/features/timeline.html" target="_blank" class="_link">{{ i18n.ts.help }}</a> + <a href="https://misskey-hub.net/docs/for-users/features/timeline/" target="_blank" class="_link">{{ i18n.ts.help }}</a> </template> </I18n> diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue index 5db2cc100a..a734f93ec9 100644 --- a/packages/frontend/src/components/MkTutorialDialog.vue +++ b/packages/frontend/src/components/MkTutorialDialog.vue @@ -130,7 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div> <I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;"> <template #link> - <a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a> + <a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a> </template> </I18n> <div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div> diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 78c62e1250..486aaa0bbd 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only <iframe ref="tweet" allow="fullscreen;web-share" - sandbox="allow-popups allow-scripts allow-same-origin" + sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin" scrolling="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`" @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div v-else> <component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> - <div v-if="thumbnail" :class="$style.thumbnail" :style="defaultStore.state.enableDataSaverMode ? '' : `background-image: url('${thumbnail}')`"> + <div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="defaultStore.state.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`"> </div> <article :class="$style.body"> <header :class="$style.header"> @@ -83,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, onUnmounted } from 'vue'; +import { defineAsyncComponent, onUnmounted, ref } from 'vue'; import type { summaly } from 'summaly'; import { url as local } from '@/config.js'; import { i18n } from '@/i18n.js'; @@ -107,35 +107,36 @@ const props = withDefaults(defineProps<{ }); const MOBILE_THRESHOLD = 500; -const isMobile = $ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD); +const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD); const self = props.url.startsWith(local); const attr = self ? 'to' : 'href'; const target = self ? null : '_blank'; -let fetching = $ref(true); -let title = $ref<string | null>(null); -let description = $ref<string | null>(null); -let thumbnail = $ref<string | null>(null); -let icon = $ref<string | null>(null); -let sitename = $ref<string | null>(null); -let player = $ref({ +const fetching = ref(true); +const title = ref<string | null>(null); +const description = ref<string | null>(null); +const thumbnail = ref<string | null>(null); +const icon = ref<string | null>(null); +const sitename = ref<string | null>(null); +const sensitive = ref<boolean>(false); +const player = ref({ url: null, width: null, height: null, } as SummalyResult['player']); -let playerEnabled = $ref(false); -let tweetId = $ref<string | null>(null); -let tweetExpanded = $ref(props.detail); +const playerEnabled = ref(false); +const tweetId = ref<string | null>(null); +const tweetExpanded = ref(props.detail); const embedId = `embed${Math.random().toString().replace(/\D/, '')}`; -let tweetHeight = $ref(150); -let unknownUrl = $ref(false); +const tweetHeight = ref(150); +const unknownUrl = ref(false); const requestUrl = new URL(props.url); if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url'); if (requestUrl.hostname === 'twitter.com' || requestUrl.hostname === 'mobile.twitter.com' || requestUrl.hostname === 'x.com' || requestUrl.hostname === 'mobile.x.com') { const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/); - if (m) tweetId = m[1]; + if (m) tweetId.value = m[1]; } if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) { @@ -147,8 +148,8 @@ requestUrl.hash = ''; window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`) .then(res => { if (!res.ok) { - fetching = false; - unknownUrl = true; + fetching.value = false; + unknownUrl.value = true; return; } @@ -156,20 +157,21 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa }) .then((info: SummalyResult) => { if (info.url == null) { - fetching = false; - unknownUrl = true; + fetching.value = false; + unknownUrl.value = true; return; } - fetching = false; - unknownUrl = false; + fetching.value = false; + unknownUrl.value = false; - title = info.title; - description = info.description; - thumbnail = info.thumbnail; - icon = info.icon; - sitename = info.sitename; - player = info.player; + title.value = info.title; + description.value = info.description; + thumbnail.value = info.thumbnail; + icon.value = info.icon; + sitename.value = info.sitename; + player.value = info.player; + sensitive.value = info.sensitive ?? false; }); function adjustTweetHeight(message: any) { @@ -178,7 +180,7 @@ function adjustTweetHeight(message: any) { if (embed?.method !== 'twttr.private.resize') return; if (embed?.id !== embedId) return; const height = embed?.params[0]?.height; - if (height) tweetHeight = height; + if (height) tweetHeight.value = height; } const openPlayer = (): void => { diff --git a/packages/frontend/src/components/MkUrlPreviewPopup.vue b/packages/frontend/src/components/MkUrlPreviewPopup.vue index 0ab012dfb7..81c383540c 100644 --- a/packages/frontend/src/components/MkUrlPreviewPopup.vue +++ b/packages/frontend/src/components/MkUrlPreviewPopup.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; @@ -28,16 +28,16 @@ const emit = defineEmits<{ }>(); const zIndex = os.claimZIndex('middle'); -let top = $ref(0); -let left = $ref(0); +const top = ref(0); +const left = ref(0); onMounted(() => { const rect = props.source.getBoundingClientRect(); const x = Math.max((rect.left + (props.source.offsetWidth / 2)) - (300 / 2), 6) + window.pageXOffset; const y = rect.top + props.source.offsetHeight + window.pageYOffset; - top = y; - left = x; + top.value = y; + left.value = x; }); </script> diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index 42ccb621b6..e1237659c2 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkButton from '@/components/MkButton.vue'; @@ -66,12 +66,12 @@ const props = defineProps<{ announcement?: any, }>(); -let dialog = $ref(null); -let title: string = $ref(props.announcement ? props.announcement.title : ''); -let text: string = $ref(props.announcement ? props.announcement.text : ''); -let icon: string = $ref(props.announcement ? props.announcement.icon : 'info'); -let display: string = $ref(props.announcement ? props.announcement.display : 'dialog'); -let needConfirmationToRead = $ref(props.announcement ? props.announcement.needConfirmationToRead : false); +const dialog = ref(null); +const title = ref<string>(props.announcement ? props.announcement.title : ''); +const text = ref<string>(props.announcement ? props.announcement.text : ''); +const icon = ref<string>(props.announcement ? props.announcement.icon : 'info'); +const display = ref<string>(props.announcement ? props.announcement.display : 'dialog'); +const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false); const emit = defineEmits<{ (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void, @@ -80,12 +80,12 @@ const emit = defineEmits<{ async function done() { const params = { - title: title, - text: text, - icon: icon, + title: title.value, + text: text.value, + icon: icon.value, imageUrl: null, - display: display, - needConfirmationToRead: needConfirmationToRead, + display: display.value, + needConfirmationToRead: needConfirmationToRead.value, userId: props.user.id, }; @@ -102,7 +102,7 @@ async function done() { }, }); - dialog.close(); + dialog.value.close(); } else { const created = await os.apiWithDialog('admin/announcements/create', params); @@ -110,14 +110,14 @@ async function done() { created: created, }); - dialog.close(); + dialog.value.close(); } } async function del() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: title }), + text: i18n.t('removeAreYouSure', { x: title.value }), }); if (canceled) return; @@ -127,7 +127,7 @@ async function del() { emit('done', { deleted: true, }); - dialog.close(); + dialog.value.close(); }); } </script> diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue index 978c5005c8..b9c7377972 100644 --- a/packages/frontend/src/components/MkUserCardMini.vue +++ b/packages/frontend/src/components/MkUserCardMini.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; import * as os from '@/os.js'; import { acct } from '@/filters/user.js'; @@ -28,14 +28,14 @@ const props = withDefaults(defineProps<{ withChart: true, }); -let chartValues = $ref<number[] | null>(null); +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 => { // 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く res.inc.splice(0, 1); - chartValues = res.inc; + chartValues.value = res.inc; }); } }); diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index 322ffee38e..4e326911d8 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -22,10 +22,10 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.statusItem"> <p :class="$style.statusItemLabel">{{ i18n.ts.notes }}</p><span :class="$style.statusItemValue">{{ number(user.notesCount) }}</span> </div> - <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem"> + <div v-if="isFollowingVisibleForMe(user)" :class="$style.statusItem"> <p :class="$style.statusItemLabel">{{ i18n.ts.following }}</p><span :class="$style.statusItemValue">{{ number(user.followingCount) }}</span> </div> - <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem"> + <div v-if="isFollowersVisibleForMe(user)" :class="$style.statusItem"> <p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span> </div> </div> @@ -40,7 +40,7 @@ import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; +import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; defineProps<{ user: Misskey.entities.UserDetailed; diff --git a/packages/frontend/src/components/MkUserOnlineIndicator.vue b/packages/frontend/src/components/MkUserOnlineIndicator.vue index c6e1218c0f..76470cba88 100644 --- a/packages/frontend/src/components/MkUserOnlineIndicator.vue +++ b/packages/frontend/src/components/MkUserOnlineIndicator.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { computed } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; @@ -24,7 +24,7 @@ const props = defineProps<{ user: Misskey.entities.User; }>(); -const text = $computed(() => { +const text = computed(() => { switch (props.user.onlineStatus) { case 'online': return i18n.ts.online; case 'active': return i18n.ts.active; diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index d958b325e5..ec2c48b1cf 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -47,11 +47,11 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div> <div>{{ number(user.notesCount) }}</div> </div> - <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem"> + <div v-if="isFollowingVisibleForMe(user)" :class="$style.statusItem"> <div :class="$style.statusItemLabel">{{ i18n.ts.following }}</div> <div>{{ number(user.followingCount) }}</div> </div> - <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem"> + <div v-if="isFollowersVisibleForMe(user)" :class="$style.statusItem"> <div :class="$style.statusItemLabel">{{ i18n.ts.followers }}</div> <div>{{ number(user.followersCount) }}</div> </div> @@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkFollowButton from '@/components/MkFollowButton.vue'; import { userPage } from '@/filters/user.js'; @@ -77,7 +77,7 @@ import number from '@/filters/number.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { $i } from '@/account.js'; -import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; +import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; const props = defineProps<{ showing: boolean; @@ -92,18 +92,18 @@ const emit = defineEmits<{ }>(); const zIndex = os.claimZIndex('middle'); -let user = $ref<Misskey.entities.UserDetailed | null>(null); -let top = $ref(0); -let left = $ref(0); +const user = ref<Misskey.entities.UserDetailed | null>(null); +const top = ref(0); +const left = ref(0); function showMenu(ev: MouseEvent) { - const { menu, cleanup } = getUserMenu(user); + const { menu, cleanup } = getUserMenu(user.value); os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup); } onMounted(() => { if (typeof props.q === 'object') { - user = props.q; + user.value = props.q; } else { const query = props.q.startsWith('@') ? Misskey.acct.parse(props.q.substring(1)) : @@ -111,7 +111,7 @@ onMounted(() => { os.api('users/show', query).then(res => { if (!props.showing) return; - user = res; + user.value = res; }); } @@ -119,8 +119,8 @@ onMounted(() => { const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset; const y = rect.top + props.source.offsetHeight + window.pageYOffset; - top = y; - left = x; + top.value = y; + left.value = x; }); </script> diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index ac38c4b62f..9d41147bd2 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkInput from '@/components/MkInput.vue'; import FormSplit from '@/components/form/split.vue'; @@ -78,43 +78,43 @@ const props = defineProps<{ includeSelf?: boolean; }>(); -let username = $ref(''); -let host = $ref(''); -let users: Misskey.entities.UserDetailed[] = $ref([]); -let recentUsers: Misskey.entities.UserDetailed[] = $ref([]); -let selected: Misskey.entities.UserDetailed | null = $ref(null); -let dialogEl = $ref(); +const username = ref(''); +const host = ref(''); +const users = ref<Misskey.entities.UserDetailed[]>([]); +const recentUsers = ref<Misskey.entities.UserDetailed[]>([]); +const selected = ref<Misskey.entities.UserDetailed | null>(null); +const dialogEl = ref(); const search = () => { - if (username === '' && host === '') { - users = []; + if (username.value === '' && host.value === '') { + users.value = []; return; } os.api('users/search-by-username-and-host', { - username: username, - host: host, + username: username.value, + host: host.value, limit: 10, detail: false, }).then(_users => { - users = _users; + users.value = _users; }); }; const ok = () => { - if (selected == null) return; - emit('ok', selected); - dialogEl.close(); + if (selected.value == null) return; + emit('ok', selected.value); + dialogEl.value.close(); // 最近使ったユーザー更新 let recents = defaultStore.state.recentlyUsedUsers; - recents = recents.filter(x => x !== selected.id); - recents.unshift(selected.id); + recents = recents.filter(x => x !== selected.value.id); + recents.unshift(selected.value.id); defaultStore.set('recentlyUsedUsers', recents.splice(0, 16)); }; const cancel = () => { emit('cancel'); - dialogEl.close(); + dialogEl.value.close(); }; onMounted(() => { @@ -122,9 +122,9 @@ onMounted(() => { userIds: defaultStore.state.recentlyUsedUsers, }).then(users => { if (props.includeSelf && users.find(x => $i ? x.id === $i.id : true) == null) { - recentUsers = [$i, ...users]; + recentUsers.value = [$i, ...users]; } else { - recentUsers = users; + recentUsers.value = users; } }); }); diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue index 4ecca7334c..5f3f5b81dd 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue @@ -34,15 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref, watch } from 'vue'; -import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; -import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; import XUser from '@/components/MkUserSetupDialog.User.vue'; -import MkInfo from '@/components/MkInfo.vue'; -import * as os from '@/os.js'; -import { $i } from '@/account.js'; import MkPagination from '@/components/MkPagination.vue'; const pinnedUsers = { endpoint: 'pinned-users', noPaging: true }; diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue index 7401dbddb1..664c4da203 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue @@ -36,18 +36,16 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref, watch } from 'vue'; -import { instance } from '@/instance.js'; +import { ref, watch } from 'vue'; 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 { $i } from '@/account.js'; -let isLocked = ref(false); -let hideOnlineStatus = ref(false); -let noCrawle = ref(false); +const isLocked = ref(false); +const hideOnlineStatus = ref(false); +const noCrawle = ref(false); watch([isLocked, hideOnlineStatus, noCrawle], () => { os.api('i/update', { diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index 8de9bbdbb1..37aa677b44 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -30,8 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref, watch } from 'vue'; -import { instance } from '@/instance.js'; +import { ref, watch } from 'vue'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue index 01a943b7a0..621995cc5b 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.User.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue @@ -29,7 +29,6 @@ import * as Misskey from 'misskey-js'; import { ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; import * as os from '@/os.js'; const props = defineProps<{ diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue index 325829a8a8..61edc345a9 100644 --- a/packages/frontend/src/components/MkVisibilityPicker.vue +++ b/packages/frontend/src/components/MkVisibilityPicker.vue @@ -42,12 +42,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick } from 'vue'; +import { nextTick, shallowRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkModal from '@/components/MkModal.vue'; import { i18n } from '@/i18n.js'; -const modal = $shallowRef<InstanceType<typeof MkModal>>(); +const modal = shallowRef<InstanceType<typeof MkModal>>(); const props = withDefaults(defineProps<{ currentVisibility: typeof Misskey.noteVisibilities[number]; @@ -62,13 +62,13 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -let v = $ref(props.currentVisibility); +const v = ref(props.currentVisibility); function choose(visibility: typeof Misskey.noteVisibilities[number]): void { - v = visibility; + v.value = visibility; emit('changeVisibility', visibility); nextTick(() => { - if (modal) modal.close(); + if (modal.value) modal.value.close(); }); } </script> diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue index 26de7dee52..746ed3e0de 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import gradient from 'chartjs-plugin-gradient'; import tinycolor from 'tinycolor2'; @@ -25,11 +25,11 @@ import { initChart } from '@/scripts/init-chart.js'; initChart(); -const chartEl = $shallowRef<HTMLCanvasElement>(null); +const chartEl = shallowRef<HTMLCanvasElement>(null); const now = new Date(); let chartInstance: Chart = null; const chartLimit = 30; -let fetching = $ref(true); +const fetching = ref(true); const { handler: externalTooltipHandler } = useChartTooltip(); @@ -65,7 +65,7 @@ async function renderChart() { const max = Math.max(...raw.read); - chartInstance = new Chart(chartEl, { + chartInstance = new Chart(chartEl.value, { type: 'bar', data: { datasets: [{ @@ -147,7 +147,7 @@ async function renderChart() { plugins: [chartVLine(vLineColor)], }); - fetching = false; + fetching.value = false; } onMounted(async () => { diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index fe76ded7b4..862a38bd54 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -54,9 +54,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import * as Misskey from 'misskey-js'; -import XTimeline from './welcome.timeline.vue'; import XSigninDialog from '@/components/MkSigninDialog.vue'; import XSignupDialog from '@/components/MkSignupDialog.vue'; import MkButton from '@/components/MkButton.vue'; @@ -66,20 +65,18 @@ import { instanceName } from '@/config.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import number from '@/filters/number.js'; import MkNumber from '@/components/MkNumber.vue'; import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue'; -let meta = $ref<Misskey.entities.Instance>(); -let stats = $ref(null); +const meta = ref<Misskey.entities.MetaResponse | null>(null); +const stats = ref<Misskey.entities.StatsResponse | null>(null); os.api('meta', { detail: true }).then(_meta => { - meta = _meta; + meta.value = _meta; }); -os.api('stats', { -}).then((res) => { - stats = res; +os.api('stats', {}).then((res) => { + stats.value = res; }); function signin() { @@ -107,35 +104,35 @@ function showMenu(ev) { action: () => { os.pageWindow('/about-sharkey'); }, - }, null, (instance.impressumUrl) ? { + }, { type: 'divider' }, (instance.impressumUrl) ? { text: i18n.ts.impressum, icon: 'ph-newspaper-clipping ph-bold ph-lg', action: () => { - window.open(instance.impressumUrl, '_blank'); + window.open(instance.impressumUrl, '_blank', 'noopener'); }, } : undefined, (instance.tosUrl) ? { text: i18n.ts.termsOfService, icon: 'ph-notebook ph-bold ph-lg', action: () => { - window.open(instance.tosUrl, '_blank'); + window.open(instance.tosUrl, '_blank', 'noopener'); }, } : undefined, (instance.privacyPolicyUrl) ? { text: i18n.ts.privacyPolicy, icon: 'ph-shield ph-bold ph-lg', action: () => { - window.open(instance.privacyPolicyUrl, '_blank'); + window.open(instance.privacyPolicyUrl, '_blank', 'noopener'); }, - } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, { + } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, { text: i18n.ts.help, icon: 'ph-question ph-bold ph-lg', action: () => { - window.open('https://misskey-hub.net/help.md', '_blank'); + window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener'); }, }], ev.currentTarget ?? ev.target); } function exploreOtherServers() { - window.open('https://joinsharkey.org/#findaninstance', '_blank'); + window.open('https://joinsharkey.org/#findaninstance', '_blank', 'noopener'); } </script> diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index d6c3e3f81d..e5b8bd9b15 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -53,10 +53,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onBeforeUnmount, onMounted, provide } from 'vue'; +import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue'; import contains from '@/scripts/contains.js'; import * as os from '@/os.js'; -import { MenuItem } from '@/types/menu'; +import { MenuItem } from '@/types/menu.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; @@ -107,18 +107,18 @@ const emit = defineEmits<{ provide('inWindow', true); -let rootEl = $shallowRef<HTMLElement | null>(); -let showing = $ref(true); +const rootEl = shallowRef<HTMLElement | null>(); +const showing = ref(true); let beforeClickedAt = 0; -let maximized = $ref(false); -let minimized = $ref(false); +const maximized = ref(false); +const minimized = ref(false); let unResizedTop = ''; let unResizedLeft = ''; let unResizedWidth = ''; let unResizedHeight = ''; function close() { - showing = false; + showing.value = false; } function onKeydown(evt) { @@ -137,46 +137,46 @@ function onContextmenu(ev: MouseEvent) { // 最前面へ移動 function top() { - if (rootEl) { - rootEl.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low'); + if (rootEl.value) { + rootEl.value.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low'); } } function maximize() { - maximized = true; - unResizedTop = rootEl.style.top; - unResizedLeft = rootEl.style.left; - unResizedWidth = rootEl.style.width; - unResizedHeight = rootEl.style.height; - rootEl.style.top = '0'; - rootEl.style.left = '0'; - rootEl.style.width = '100%'; - rootEl.style.height = '100%'; + maximized.value = true; + unResizedTop = rootEl.value.style.top; + unResizedLeft = rootEl.value.style.left; + unResizedWidth = rootEl.value.style.width; + unResizedHeight = rootEl.value.style.height; + rootEl.value.style.top = '0'; + rootEl.value.style.left = '0'; + rootEl.value.style.width = '100%'; + rootEl.value.style.height = '100%'; } function unMaximize() { - maximized = false; - rootEl.style.top = unResizedTop; - rootEl.style.left = unResizedLeft; - rootEl.style.width = unResizedWidth; - rootEl.style.height = unResizedHeight; + maximized.value = false; + rootEl.value.style.top = unResizedTop; + rootEl.value.style.left = unResizedLeft; + rootEl.value.style.width = unResizedWidth; + rootEl.value.style.height = unResizedHeight; } function minimize() { - minimized = true; - unResizedWidth = rootEl.style.width; - unResizedHeight = rootEl.style.height; - rootEl.style.width = minWidth + 'px'; - rootEl.style.height = props.mini ? '32px' : '39px'; + minimized.value = true; + unResizedWidth = rootEl.value.style.width; + unResizedHeight = rootEl.value.style.height; + rootEl.value.style.width = minWidth + 'px'; + rootEl.value.style.height = props.mini ? '32px' : '39px'; } function unMinimize() { - const main = rootEl; + const main = rootEl.value; if (main == null) return; - minimized = false; - rootEl.style.width = unResizedWidth; - rootEl.style.height = unResizedHeight; + minimized.value = false; + rootEl.value.style.width = unResizedWidth; + rootEl.value.style.height = unResizedHeight; const browserWidth = window.innerWidth; const browserHeight = window.innerHeight; const windowWidth = main.offsetWidth; @@ -192,7 +192,7 @@ function onBodyMousedown() { } function onDblClick() { - if (minimized) { + if (minimized.value) { unMinimize(); } else { maximize(); @@ -205,7 +205,7 @@ function onHeaderMousedown(evt: MouseEvent) { let beforeMaximized = false; - if (maximized) { + if (maximized.value) { beforeMaximized = true; unMaximize(); } @@ -219,7 +219,7 @@ function onHeaderMousedown(evt: MouseEvent) { beforeClickedAt = Date.now(); - const main = rootEl; + const main = rootEl.value; if (main == null) return; if (!contains(main, document.activeElement)) main.focus(); @@ -251,8 +251,8 @@ function onHeaderMousedown(evt: MouseEvent) { // 右はみ出し if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth; - rootEl.style.left = moveLeft + 'px'; - rootEl.style.top = moveTop + 'px'; + rootEl.value.style.left = moveLeft + 'px'; + rootEl.value.style.top = moveTop + 'px'; } if (beforeMaximized) { @@ -270,7 +270,7 @@ function onHeaderMousedown(evt: MouseEvent) { // 上ハンドル掴み時 function onTopHandleMousedown(evt) { - const main = rootEl; + const main = rootEl.value; // どういうわけかnullになることがある if (main == null) return; @@ -298,7 +298,7 @@ function onTopHandleMousedown(evt) { // 右ハンドル掴み時 function onRightHandleMousedown(evt) { - const main = rootEl; + const main = rootEl.value; if (main == null) return; const base = evt.clientX; @@ -323,7 +323,7 @@ function onRightHandleMousedown(evt) { // 下ハンドル掴み時 function onBottomHandleMousedown(evt) { - const main = rootEl; + const main = rootEl.value; if (main == null) return; const base = evt.clientY; @@ -348,7 +348,7 @@ function onBottomHandleMousedown(evt) { // 左ハンドル掴み時 function onLeftHandleMousedown(evt) { - const main = rootEl; + const main = rootEl.value; if (main == null) return; const base = evt.clientX; @@ -400,27 +400,27 @@ function onBottomLeftHandleMousedown(evt) { // 高さを適用 function applyTransformHeight(height) { if (height > window.innerHeight) height = window.innerHeight; - rootEl.style.height = height + 'px'; + rootEl.value.style.height = height + 'px'; } // 幅を適用 function applyTransformWidth(width) { if (width > window.innerWidth) width = window.innerWidth; - rootEl.style.width = width + 'px'; + rootEl.value.style.width = width + 'px'; } // Y座標を適用 function applyTransformTop(top) { - rootEl.style.top = top + 'px'; + rootEl.value.style.top = top + 'px'; } // X座標を適用 function applyTransformLeft(left) { - rootEl.style.left = left + 'px'; + rootEl.value.style.left = left + 'px'; } function onBrowserResize() { - const main = rootEl; + const main = rootEl.value; if (main == null) return; const position = main.getBoundingClientRect(); @@ -438,8 +438,8 @@ onMounted(() => { applyTransformWidth(props.initialWidth); if (props.initialHeight) applyTransformHeight(props.initialHeight); - applyTransformTop((window.innerHeight / 2) - (rootEl.offsetHeight / 2)); - applyTransformLeft((window.innerWidth / 2) - (rootEl.offsetWidth / 2)); + applyTransformTop((window.innerHeight / 2) - (rootEl.value.offsetHeight / 2)); + applyTransformLeft((window.innerWidth / 2) - (rootEl.value.offsetWidth / 2)); // 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする top(); diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue index 7460515c33..a9b2e8a00d 100644 --- a/packages/frontend/src/components/MkYouTubePlayer.vue +++ b/packages/frontend/src/components/MkYouTubePlayer.vue @@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref } from 'vue'; import MkWindow from '@/components/MkWindow.vue'; import { versatileLang } from '@/scripts/intl-const.js'; import { defaultStore } from '@/store.js'; @@ -35,22 +36,22 @@ const props = defineProps<{ const requestUrl = new URL(props.url); if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url'); -let fetching = $ref(true); -let title = $ref<string | null>(null); -let player = $ref({ +const fetching = ref(true); +const title = ref<string | null>(null); +const player = ref({ url: null, width: null, height: null, }); const ytFetch = (): void => { - fetching = true; + fetching.value = true; window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`).then(res => { res.json().then(info => { if (info.url == null) return; - title = info.title; - fetching = false; - player = info.player; + title.value = info.title; + fetching.value = false; + player.value = info.player; }); }); }; diff --git a/packages/frontend/src/components/SkApprovalUser.vue b/packages/frontend/src/components/SkApprovalUser.vue index 99dcb717b1..2bf6361ac8 100644 --- a/packages/frontend/src/components/SkApprovalUser.vue +++ b/packages/frontend/src/components/SkApprovalUser.vue @@ -27,6 +27,7 @@ </template> <script lang="ts" setup> +import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; @@ -37,15 +38,15 @@ const props = defineProps<{ user: Misskey.entities.User; }>(); -let reason = $ref(''); -let email = $ref(''); +let reason = ref(''); +let email = ref(''); function getReason() { return os.api('admin/show-user', { userId: props.user.id, }).then(info => { - reason = info?.signupReason; - email = info?.email; + reason.value = info?.signupReason; + email.value = info?.email; }); } diff --git a/packages/frontend/src/components/SkInstanceTicker.vue b/packages/frontend/src/components/SkInstanceTicker.vue index 4e2856388e..fa7b2a444d 100644 --- a/packages/frontend/src/components/SkInstanceTicker.vue +++ b/packages/frontend/src/components/SkInstanceTicker.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { computed } from 'vue'; import { instanceName } from '@/config.js'; import { instance as Instance } from '@/instance.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; @@ -30,7 +30,7 @@ const instance = props.instance ?? { themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content, }; -const faviconUrl = $computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico'); +const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico'); const themeColor = instance.themeColor ?? '#777777'; diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index b308f4a07a..cb37861330 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div - v-if="!muted" + v-if="!hardMuted && !muted" v-show="!isDeleted" ref="el" v-hotkey="keymap" @@ -58,9 +58,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div style="container-type: inline-size;"> <p v-if="appearNote.cw != null" :class="$style.cw"> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> - <MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;" v-on:click.stop/> + <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/> </p> - <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]" > + <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> <div :class="$style.text"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <Mfm @@ -81,31 +81,31 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> </div> </div> - <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> - <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> + <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> + <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> </div> <div v-if="appearNote.files.length > 0"> - <MkMediaList :mediaList="appearNote.files" v-on:click.stop/> + <MkMediaList :mediaList="appearNote.files" @click.stop/> </div> - <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" v-on:click.stop /> - <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" v-on:click.stop/> + <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" @click.stop/> + <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" @click.stop/> <div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> - <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" v-on:click.stop @click="collapsed = false"> + <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false"> <span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span> </button> - <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" v-on:click.stop @click="collapsed = true"> + <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click.stop @click="collapsed = true"> <span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span> </button> </div> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> </div> - <MkReactionsViewer :note="appearNote" :maxNumber="16" v-on:click.stop @mockUpdateMyReaction="emitUpdReaction"> + <MkReactionsViewer :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction"> <template #more> <div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div> </template> </MkReactionsViewer> <footer :class="$style.footer"> - <button :class="$style.footerButton" class="_button" v-on:click.stop @click="reply()"> + <button :class="$style.footerButton" class="_button" @click.stop @click="reply()"> <i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p> </button> @@ -115,7 +115,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="$style.footerButton" class="_button" :style="renoted ? 'color: var(--accent) !important;' : ''" - v-on:click.stop + @click.stop @mousedown="renoted ? undoRenote(appearNote) : boostVisibility()" > <i class="ph-rocket-launch ph-bold ph-lg"></i> @@ -129,19 +129,19 @@ SPDX-License-Identifier: AGPL-3.0-only ref="quoteButton" :class="$style.footerButton" class="_button" - v-on:click.stop + @click.stop @mousedown="quote()" > <i class="ph-quotes ph-bold ph-lg"></i> </button> - <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" v-on:click.stop @click="like()"> + <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()"> <i class="ph-heart ph-bold ph-lg"></i> </button> <button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()"> <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> </button> - <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" v-on:click.stop @click="undoReact(appearNote)"> + <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click.stop @click="undoReact(appearNote)"> <i class="ph-minus ph-bold ph-lg"></i> </button> <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()"> @@ -154,7 +154,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </article> </div> -<div v-else :class="$style.muted" @click="muted = false"> +<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false"> <I18n :src="i18n.ts.userSaysSomething" tag="small"> <template #name> <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)"> @@ -163,10 +163,16 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </I18n> </div> +<div v-else> + <!-- + MkDateSeparatedList uses TransitionGroup which requires single element in the child elements + so MkNote create empty div instead of no elements + --> +</div> </template> <script lang="ts" setup> -import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent, watch, provide } from 'vue'; +import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue'; import * as mfm from '@sharkey/sfm-js'; import * as Misskey from 'misskey-js'; import SkNoteSub from '@/components/SkNoteSub.vue'; @@ -184,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 * as sound from '@/scripts/sound.js'; import { defaultStore, noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; @@ -207,6 +214,7 @@ const props = withDefaults(defineProps<{ note: Misskey.entities.Note; pinned?: boolean; mock?: boolean; + withHardMute?: boolean; }>(), { mock: false, }); @@ -223,7 +231,7 @@ const router = useRouter(); const inChannel = inject('inChannel', null); const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); -let note = $ref(deepClone(props.note)); +const note = ref(deepClone(props.note)); function noteclick(id: string) { const selection = document.getSelection(); @@ -235,7 +243,7 @@ function noteclick(id: string) { // plugin if (noteViewInterruptors.length > 0) { onMounted(async () => { - let result: Misskey.entities.Note | null = deepClone(note); + let result: Misskey.entities.Note | null = deepClone(note.value); for (const interruptor of noteViewInterruptors) { try { result = await interruptor.handler(result); @@ -247,15 +255,16 @@ if (noteViewInterruptors.length > 0) { console.error(err); } } - note = result; + note.value = result; }); } const isRenote = ( - note.renote != null && - note.text == null && - note.fileIds.length === 0 && - note.poll == null + note.value.renote != null && + note.value.text == null && + note.value.cw == null && + note.value.fileIds.length === 0 && + note.value.poll == null ); const el = shallowRef<HTMLElement>(); @@ -267,27 +276,37 @@ const reactButton = shallowRef<HTMLElement>(); const quoteButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>(); const likeButton = shallowRef<HTMLElement>(); -let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note); -const renoteUrl = appearNote.renote ? appearNote.renote.url : null; -const renoteUri = appearNote.renote ? appearNote.renote.uri : null; +const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null; +const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null; -const isMyRenote = $i && ($i.id === note.userId); +const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(defaultStore.state.uncollapseCW); -const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null); -const urls = $computed(() => parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null); -const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null); -const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); -const isLong = shouldCollapsed(appearNote, urls ?? []); -const collapsed = defaultStore.state.expandLongNote && appearNote.cw == null ? false : ref(appearNote.cw == null && isLong); +const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text).filter(u => u !== renoteUrl && u !== renoteUri) : null); +const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null); +const isLong = shouldCollapsed(appearNote.value, urls.value ?? []); +const collapsed = defaultStore.state.expandLongNote && appearNote.value.cw == null ? false : ref(appearNote.value.cw == null && isLong); const isDeleted = ref(false); const renoted = ref(false); -const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); +const muted = ref(checkMute(appearNote.value, $i?.mutedWords)); +const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords)); const translation = ref<any>(null); const translating = ref(false); -const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); -const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id)); -let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null))); +const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); +const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id)); +const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null))); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); +const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null); +const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); + +function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean { + if (mutedWords == null) return false; + + if (checkWordMute(note, $i, mutedWords)) return true; + if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true; + if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true; + return false; +} const keymap = { 'r': () => reply(true), @@ -302,20 +321,20 @@ const keymap = { provide('react', (reaction: string) => { os.api('notes/reactions/create', { - noteId: appearNote.id, + noteId: appearNote.value.id, reaction: reaction, }); }); if (props.mock) { watch(() => props.note, (to) => { - note = deepClone(to); + note.value = deepClone(to); }, { deep: true }); } else { useNoteCapture({ rootEl: el, - note: $$(appearNote), - pureNote: $$(note), + note: appearNote, + pureNote: note, isDeletedRef: isDeleted, }); } @@ -323,7 +342,7 @@ if (props.mock) { if (!props.mock) { useTooltip(renoteButton, async (showing) => { const renotes = await os.api('notes/renotes', { - noteId: appearNote.id, + noteId: appearNote.value.id, limit: 11, }); @@ -334,14 +353,14 @@ if (!props.mock) { os.popup(MkUsersTooltip, { showing, users, - count: appearNote.renoteCount, + count: appearNote.value.renoteCount, targetElement: renoteButton.value, }, {}, 'closed'); }); useTooltip(quoteButton, async (showing) => { const renotes = await os.api('notes/renotes', { - noteId: appearNote.id, + noteId: appearNote.value.id, limit: 11, quote: true, }); @@ -353,14 +372,14 @@ if (!props.mock) { os.popup(MkUsersTooltip, { showing, users, - count: appearNote.renoteCount, + count: appearNote.value.renoteCount, targetElement: quoteButton.value, }, {}, 'closed'); }); if ($i) { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, }).then((res) => { @@ -420,7 +439,7 @@ function renote(visibility: Visibility | 'local') { pleaseLogin(); showMovedDialog(); - if (appearNote.channel) { + if (appearNote.value.channel) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -431,14 +450,14 @@ function renote(visibility: Visibility | 'local') { if (!props.mock) { os.api('notes/create', { - renoteId: appearNote.id, - channelId: appearNote.channelId, + renoteId: appearNote.value.id, + channelId: appearNote.value.channelId, }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; }); } - } else if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) { + } else if (!appearNote.value.channel || appearNote.value.channel?.allowRenoteToExternal) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -450,16 +469,16 @@ function renote(visibility: Visibility | 'local') { const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility; const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly; - let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility); - if (appearNote.channel?.isSensitive) { - noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.visibility : visibility, 'home'); + let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility); + if (appearNote.value.channel?.isSensitive) { + noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home'); } if (!props.mock) { os.api('notes/create', { localOnly: visibility === 'local' ? true : localOnlySetting, visibility: noteVisibility, - renoteId: appearNote.id, + renoteId: appearNote.value.id, }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; @@ -475,13 +494,13 @@ function quote() { return; } - if (appearNote.channel) { + if (appearNote.value.channel) { os.post({ - renote: appearNote, - channel: appearNote.channel, + renote: appearNote.value, + channel: appearNote.value.channel, }).then(() => { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, quote: true, @@ -500,10 +519,10 @@ function quote() { }); } else { os.post({ - renote: appearNote, + renote: appearNote.value, }).then(() => { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, quote: true, @@ -529,8 +548,8 @@ function reply(viaKeyboard = false): void { return; } os.post({ - reply: appearNote, - channel: appearNote.channel, + reply: appearNote.value, + channel: appearNote.value.channel, animation: !viaKeyboard, }, () => { focus(); @@ -544,7 +563,7 @@ function like(): void { return; } os.api('notes/like', { - noteId: appearNote.id, + noteId: appearNote.value.id, override: defaultLike.value, }); const el = likeButton.value as HTMLElement | null | undefined; @@ -559,13 +578,15 @@ function like(): void { function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); - if (appearNote.reactionAcceptance === 'likeOnly') { + if (appearNote.value.reactionAcceptance === 'likeOnly') { + sound.play('reaction'); + if (props.mock) { return; } os.api('notes/like', { - noteId: appearNote.id, + noteId: appearNote.value.id, override: defaultLike.value, }); const el = reactButton.value as HTMLElement | null | undefined; @@ -578,16 +599,18 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { + sound.play('reaction'); + if (props.mock) { emit('reaction', reaction); return; } os.api('notes/reactions/create', { - noteId: appearNote.id, + noteId: appearNote.value.id, reaction: reaction, }); - if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { + if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) { claimAchievement('reactWithoutRead'); } }, () => { @@ -614,8 +637,8 @@ function undoRenote(note) : void { if (props.mock) { return; } - os.api("notes/unrenote", { - noteId: note.id + os.api('notes/unrenote', { + noteId: note.id, }); os.toast(i18n.ts.rmboost); renoted.value = false; @@ -649,7 +672,7 @@ function onContextmenu(ev: MouseEvent): void { ev.preventDefault(); react(); } else { - const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); + const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); os.contextMenu(menu, ev).then(focus).finally(cleanup); } } @@ -659,14 +682,14 @@ function menu(viaKeyboard = false): void { return; } - const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); + const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); os.popupMenu(menu, menuButton.value, { viaKeyboard, }).then(focus).finally(cleanup); } async function menuVersions(viaKeyboard = false): Promise<void> { - const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton }); + const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton }); os.popupMenu(menu, menuVersionsButton.value, { viaKeyboard, }).then(focus).finally(cleanup); @@ -677,7 +700,7 @@ async function clip() { return; } - os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); + os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); } function showRenoteMenu(viaKeyboard = false): void { @@ -692,7 +715,7 @@ function showRenoteMenu(viaKeyboard = false): void { danger: true, action: () => { os.api('notes/delete', { - noteId: note.id, + noteId: note.value.id, }); isDeleted.value = true; }, @@ -702,17 +725,17 @@ function showRenoteMenu(viaKeyboard = false): void { if (isMyRenote) { pleaseLogin(); os.popupMenu([ - getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote), - null, + getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), + { type: 'divider' }, getUnrenote(), ], renoteTime.value, { viaKeyboard: viaKeyboard, }); } else { os.popupMenu([ - getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote), - null, - getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote), + getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), + { type: 'divider' }, + getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote), $i.isModerator || $i.isAdmin ? getUnrenote() : undefined, ], renoteTime.value, { viaKeyboard: viaKeyboard, @@ -750,7 +773,7 @@ function focusAfter() { function readPromo() { os.api('promo/read', { - noteId: appearNote.id, + noteId: appearNote.value.id, }); isDeleted.value = true; } diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index 4699eba8f6..8bf9e244e0 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.noteContent"> <p v-if="appearNote.cw != null" :class="$style.cw"> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> - <MkCwButton v-model="showContent" :note="appearNote"/> + <MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/> </p> <div v-show="appearNote.cw == null || showContent"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> @@ -101,8 +101,8 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> </div> </div> - <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> - <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> + <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> + <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> <div v-if="appearNote.files.length > 0"> <MkMediaList :mediaList="appearNote.files"/> </div> @@ -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 * as sound from '@/scripts/sound.js'; import { defaultStore, noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; @@ -256,12 +257,11 @@ import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import { claimAchievement } from '@/scripts/achievements.js'; -import { MenuItem } from '@/types/menu.js'; import { checkAnimationFromMfm } from '@/scripts/check-animated-mfm.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; -import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import MkPagination from '@/components/MkPagination.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; @@ -272,12 +272,12 @@ const props = defineProps<{ const inChannel = inject('inChannel', null); -let note = $ref(deepClone(props.note)); +const note = ref(deepClone(props.note)); // plugin if (noteViewInterruptors.length > 0) { onMounted(async () => { - let result: Misskey.entities.Note | null = deepClone(note); + let result: Misskey.entities.Note | null = deepClone(note.value); for (const interruptor of noteViewInterruptors) { try { result = await interruptor.handler(result); @@ -289,15 +289,15 @@ if (noteViewInterruptors.length > 0) { console.error(err); } } - note = result; + note.value = result; }); } const isRenote = ( - note.renote != null && - note.text == null && - note.fileIds.length === 0 && - note.poll == null + note.value.renote != null && + note.value.text == null && + note.value.fileIds.length === 0 && + note.value.poll == null ); const el = shallowRef<HTMLElement>(); @@ -309,26 +309,25 @@ const reactButton = shallowRef<HTMLElement>(); const quoteButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>(); const likeButton = shallowRef<HTMLElement>(); -let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note); -const renoteUrl = appearNote.renote ? appearNote.renote.url : null; -const renoteUri = appearNote.renote ? appearNote.renote.uri : null; - -const isMyRenote = $i && ($i.id === note.userId); +const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null; +const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null; +const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(defaultStore.state.uncollapseCW); const isDeleted = ref(false); const renoted = ref(false); -const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); +const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false); const translation = ref(null); const translating = ref(false); -const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null); +const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; const urls = parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null; -const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null); +const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null); const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); -const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); +const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); const conversation = ref<Misskey.entities.Note[]>([]); const replies = ref<Misskey.entities.Note[]>([]); const quotes = ref<Misskey.entities.Note[]>([]); -const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id); +const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i.id); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); watch(() => props.expandAllCws, (expandAllCws) => { @@ -336,8 +335,8 @@ watch(() => props.expandAllCws, (expandAllCws) => { }); if ($i) { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, }).then((res) => { @@ -356,41 +355,41 @@ const keymap = { provide('react', (reaction: string) => { os.api('notes/reactions/create', { - noteId: appearNote.id, + noteId: appearNote.value.id, reaction: reaction, }); }); -let tab = $ref('replies'); -let reactionTabType = $ref(null); +const tab = ref('replies'); +const reactionTabType = ref(null); -const renotesPagination = $computed(() => ({ +const renotesPagination = computed(() => ({ endpoint: 'notes/renotes', limit: 10, params: { - noteId: appearNote.id, + noteId: appearNote.value.id, }, })); -const reactionsPagination = $computed(() => ({ +const reactionsPagination = computed(() => ({ endpoint: 'notes/reactions', limit: 10, params: { - noteId: appearNote.id, - type: reactionTabType, + noteId: appearNote.value.id, + type: reactionTabType.value, }, })); useNoteCapture({ rootEl: el, - note: $$(appearNote), - pureNote: $$(note), + note: appearNote, + pureNote: note, isDeletedRef: isDeleted, }); useTooltip(renoteButton, async (showing) => { const renotes = await os.api('notes/renotes', { - noteId: appearNote.id, + noteId: appearNote.value.id, limit: 11, }); @@ -401,14 +400,14 @@ useTooltip(renoteButton, async (showing) => { os.popup(MkUsersTooltip, { showing, users, - count: appearNote.renoteCount, + count: appearNote.value.renoteCount, targetElement: renoteButton.value, }, {}, 'closed'); }); useTooltip(quoteButton, async (showing) => { const renotes = await os.api('notes/renotes', { - noteId: appearNote.id, + noteId: appearNote.value.id, limit: 11, quote: true, }); @@ -420,7 +419,7 @@ useTooltip(quoteButton, async (showing) => { os.popup(MkUsersTooltip, { showing, users, - count: appearNote.renoteCount, + count: appearNote.value.renoteCount, targetElement: quoteButton.value, }, {}, 'closed'); }); @@ -475,7 +474,7 @@ function renote(visibility: Visibility | 'local') { pleaseLogin(); showMovedDialog(); - if (appearNote.channel) { + if (appearNote.value.channel) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -485,13 +484,13 @@ function renote(visibility: Visibility | 'local') { } os.api('notes/create', { - renoteId: appearNote.id, - channelId: appearNote.channelId, + renoteId: appearNote.value.id, + channelId: appearNote.value.channelId, }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; }); - } else if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) { + } else if (!appearNote.value.channel || appearNote.value.channel?.allowRenoteToExternal) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -503,15 +502,15 @@ function renote(visibility: Visibility | 'local') { const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility; const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly; - let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility); - if (appearNote.channel?.isSensitive) { - noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.visibility : visibility, 'home'); + let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility); + if (appearNote.value.channel?.isSensitive) { + noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home'); } os.api('notes/create', { localOnly: visibility === 'local' ? true : localOnlySetting, visibility: noteVisibility, - renoteId: appearNote.id, + renoteId: appearNote.value.id, }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; @@ -523,13 +522,13 @@ function quote() { pleaseLogin(); showMovedDialog(); - if (appearNote.channel) { + if (appearNote.value.channel) { os.post({ - renote: appearNote, - channel: appearNote.channel, + renote: appearNote.value, + channel: appearNote.value.channel, }).then(() => { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, quote: true, @@ -548,10 +547,10 @@ function quote() { }); } else { os.post({ - renote: appearNote, + renote: appearNote.value, }).then(() => { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, quote: true, @@ -575,8 +574,8 @@ function reply(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); os.post({ - reply: appearNote, - channel: appearNote.channel, + reply: appearNote.value, + channel: appearNote.value.channel, animation: !viaKeyboard, }, () => { focus(); @@ -586,9 +585,9 @@ function reply(viaKeyboard = false): void { function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); - if (appearNote.reactionAcceptance === 'likeOnly') { + if (appearNote.value.reactionAcceptance === 'likeOnly') { os.api('notes/like', { - noteId: appearNote.id, + noteId: appearNote.value.id, override: defaultLike.value, }); const el = reactButton.value as HTMLElement | null | undefined; @@ -601,11 +600,13 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { + sound.play('reaction'); + os.api('notes/reactions/create', { - noteId: appearNote.id, + noteId: appearNote.value.id, reaction: reaction, }); - if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { + if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) { claimAchievement('reactWithoutRead'); } }, () => { @@ -618,7 +619,7 @@ function like(): void { pleaseLogin(); showMovedDialog(); os.api('notes/like', { - noteId: appearNote.id, + noteId: appearNote.value.id, override: defaultLike.value, }); const el = likeButton.value as HTMLElement | null | undefined; @@ -640,8 +641,8 @@ function undoReact(note): void { function undoRenote() : void { if (!renoted.value) return; - os.api("notes/unrenote", { - noteId: appearNote.id, + os.api('notes/unrenote', { + noteId: appearNote.value.id, }); os.toast(i18n.ts.rmboost); renoted.value = false; @@ -669,27 +670,27 @@ function onContextmenu(ev: MouseEvent): void { ev.preventDefault(); react(); } else { - const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }); + const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted }); os.contextMenu(menu, ev).then(focus).finally(cleanup); } } function menu(viaKeyboard = false): void { - const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }); + const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted }); os.popupMenu(menu, menuButton.value, { viaKeyboard, }).then(focus).finally(cleanup); } async function menuVersions(viaKeyboard = false): Promise<void> { - const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton }); + const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton }); os.popupMenu(menu, menuVersionsButton.value, { viaKeyboard, }).then(focus).finally(cleanup); } async function clip() { - os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus); + os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus); } function showRenoteMenu(viaKeyboard = false): void { @@ -701,7 +702,7 @@ function showRenoteMenu(viaKeyboard = false): void { danger: true, action: () => { os.api('notes/delete', { - noteId: note.id, + noteId: note.value.id, }); isDeleted.value = true; }, @@ -723,7 +724,7 @@ const repliesLoaded = ref(false); function loadReplies() { repliesLoaded.value = true; os.api('notes/children', { - noteId: appearNote.id, + noteId: appearNote.value.id, limit: 30, showQuotes: false, }).then(res => { @@ -738,7 +739,7 @@ const quotesLoaded = ref(false); function loadQuotes() { quotesLoaded.value = true; os.api('notes/renotes', { - noteId: appearNote.id, + noteId: appearNote.value.id, limit: 30, quote: true, }).then(res => { @@ -753,13 +754,13 @@ const conversationLoaded = ref(false); function loadConversation() { conversationLoaded.value = true; os.api('notes/conversation', { - noteId: appearNote.replyId, + noteId: appearNote.value.replyId, }).then(res => { conversation.value = res.reverse(); }); } -if (appearNote.reply && appearNote.reply.replyId && defaultStore.state.autoloadConversation) loadConversation(); +if (appearNote.value.reply && appearNote.value.reply.replyId && defaultStore.state.autoloadConversation) loadConversation(); function animatedMFM() { if (allowAnim.value) { diff --git a/packages/frontend/src/components/SkNoteSimple.vue b/packages/frontend/src/components/SkNoteSimple.vue index 05a19e291d..fe12baedeb 100644 --- a/packages/frontend/src/components/SkNoteSimple.vue +++ b/packages/frontend/src/components/SkNoteSimple.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div> <p v-if="note.cw != null" :class="$style.cw"> <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> - <MkCwButton v-model="showContent" :note="note" v-on:click.stop/> + <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll" @click.stop/> </p> <div v-show="note.cw == null || showContent"> <MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note"/> @@ -22,12 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkCwButton from '@/components/MkCwButton.vue'; -import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; const props = defineProps<{ @@ -36,10 +35,10 @@ const props = defineProps<{ hideFiles?: boolean; }>(); -let showContent = $ref(defaultStore.state.uncollapseCW); +let showContent = ref(defaultStore.state.uncollapseCW); watch(() => props.expandAllCws, (expandAllCws) => { - if (expandAllCws !== showContent) showContent = expandAllCws; + if (expandAllCws !== showContent.value) showContent.value = expandAllCws; }); </script> diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue index dd4abe8f58..fc30dc87aa 100644 --- a/packages/frontend/src/components/SkNoteSub.vue +++ b/packages/frontend/src/components/SkNoteSub.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.content"> <p v-if="note.cw != null" :class="$style.cw"> <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/> - <MkCwButton v-model="showContent" :note="note"/> + <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/> </p> <div v-show="note.cw == null || showContent"> <MkSubNoteContent :class="$style.text" :note="note" :translating="translating" :translation="translation"/> @@ -101,15 +101,14 @@ import { notePage } from '@/filters/note.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { userPage } from "@/filters/user.js"; -import { checkWordMute } from "@/scripts/check-word-mute.js"; -import { defaultStore } from "@/store.js"; +import { userPage } from '@/filters/user.js'; +import { checkWordMute } from '@/scripts/check-word-mute.js'; +import { defaultStore } from '@/store.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { claimAchievement } from '@/scripts/achievements.js'; -import type { MenuItem } from '@/types/menu.js'; import { getNoteMenu } from '@/scripts/get-note-menu.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js'; @@ -140,7 +139,7 @@ const quoteButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>(); const likeButton = shallowRef<HTMLElement>(); -let appearNote = $computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note); +let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); const isRenote = ( @@ -152,13 +151,13 @@ const isRenote = ( useNoteCapture({ rootEl: el, - note: $$(appearNote), + note: appearNote, isDeletedRef: isDeleted, }); if ($i) { - os.api("notes/renotes", { - noteId: appearNote.id, + os.api('notes/renotes', { + noteId: appearNote.value.id, userId: $i.id, limit: 1, }).then((res) => { @@ -239,8 +238,8 @@ function undoReact(note): void { function undoRenote() : void { if (!renoted.value) return; - os.api("notes/unrenote", { - noteId: appearNote.id, + os.api('notes/unrenote', { + noteId: appearNote.value.id, }); os.toast(i18n.ts.rmboost); renoted.value = false; @@ -254,13 +253,13 @@ function undoRenote() : void { } } -let showContent = $ref(defaultStore.state.uncollapseCW); +let showContent = ref(defaultStore.state.uncollapseCW); watch(() => props.expandAllCws, (expandAllCws) => { - if (expandAllCws !== showContent) showContent = expandAllCws; + if (expandAllCws !== showContent.value) showContent.value = expandAllCws; }); -let replies: Misskey.entities.Note[] = $ref([]); +let replies = ref<Misskey.entities.Note[]>([]); function boostVisibility() { os.popupMenu([ @@ -302,7 +301,7 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc pleaseLogin(); showMovedDialog(); - if (appearNote.channel) { + if (appearNote.value.channel) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -342,12 +341,12 @@ function quote() { pleaseLogin(); showMovedDialog(); - if (appearNote.channel) { + if (appearNote.value.channel) { os.post({ - renote: appearNote, - channel: appearNote.channel, + renote: appearNote.value, + channel: appearNote.value.channel, }).then(() => { - os.api("notes/renotes", { + os.api('notes/renotes', { noteId: props.note.id, userId: $i.id, limit: 1, @@ -367,9 +366,9 @@ function quote() { }); } else { os.post({ - renote: appearNote, + renote: appearNote.value, }).then(() => { - os.api("notes/renotes", { + os.api('notes/renotes', { noteId: props.note.id, userId: $i.id, limit: 1, @@ -403,7 +402,7 @@ if (props.detail) { limit: numberOfReplies.value, showQuotes: false, }).then(res => { - replies = res; + replies.value = res; }); } </script> diff --git a/packages/frontend/src/components/SkOldNoteWindow.vue b/packages/frontend/src/components/SkOldNoteWindow.vue index 49fbd39812..237032c9d5 100644 --- a/packages/frontend/src/components/SkOldNoteWindow.vue +++ b/packages/frontend/src/components/SkOldNoteWindow.vue @@ -30,7 +30,7 @@ <div :class="$style.noteContent"> <p v-if="appearNote.cw != null" :class="$style.cw"> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/> - <MkCwButton v-model="showContent" :note="appearNote"/> + <MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/> </p> <div v-show="appearNote.cw == null || showContent"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> @@ -76,7 +76,7 @@ </template> <script lang="ts" setup> -import { inject, onMounted, ref, shallowRef } from 'vue'; +import { inject, onMounted, ref, shallowRef, computed } from 'vue'; import * as mfm from '@sharkey/sfm-js'; import * as Misskey from 'misskey-js'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; @@ -89,7 +89,6 @@ import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import { userPage } from '@/filters/user.js'; import { defaultStore, noteViewInterruptors } from '@/store.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; -import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { deepClone } from '@/scripts/clone.js'; import { dateTimeFormat } from '@/scripts/intl-const.js'; @@ -106,42 +105,42 @@ const emit = defineEmits<{ const inChannel = inject('inChannel', null); -let note = $ref(deepClone(props.note)); +let note = ref(deepClone(props.note)); // plugin if (noteViewInterruptors.length > 0) { onMounted(async () => { - let result = deepClone(note); + let result = deepClone(note.value); for (const interruptor of noteViewInterruptors) { result = await interruptor.handler(result); } - note = result; + note.value = result; }); } const replaceContent = () => { - props.oldText ? note.text = props.oldText : undefined; - note.createdAt = props.updatedAt; + props.oldText ? note.value.text = props.oldText : undefined; + note.value.createdAt = props.updatedAt; }; replaceContent(); const isRenote = ( - note.renote != null && - note.text == null && - note.fileIds.length === 0 && - note.poll == null + note.value.renote != null && + note.value.text == null && + note.value.fileIds.length === 0 && + note.value.poll == null ); const el = shallowRef<HTMLElement>(); -let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note); -const renoteUrl = appearNote.renote ? appearNote.renote.url : null; -const renoteUri = appearNote.renote ? appearNote.renote.uri : null; +let appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note); +const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null; +const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null; const showContent = ref(false); const translation = ref(null); const translating = ref(false); -const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).filter(u => u !== renoteUrl && u !== renoteUri) : null; -const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); +const urls = appearNote.value.text ? extractUrlFromMfm(mfm.parse(appearNote.value.text)).filter(u => u !== renoteUrl && u !== renoteUri) : null; +const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); </script> diff --git a/packages/frontend/src/components/form/section.vue b/packages/frontend/src/components/form/section.vue index 095b24604a..6af63d1ec6 100644 --- a/packages/frontend/src/components/form/section.vue +++ b/packages/frontend/src/components/form/section.vue @@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="[$style.root, { [$style.rootFirst]: first }]"> <div :class="[$style.label, { [$style.labelFirst]: first }]"><slot name="label"></slot></div> + <div :class="[$style.description]"><slot name="description"></slot></div> <div :class="$style.main"> <slot></slot> </div> @@ -31,7 +32,7 @@ defineProps<{ .label { font-weight: bold; padding: 1.5em 0 0 0; - margin: 0 0 16px 0; + margin: 0 0 8px 0; &:empty { display: none; @@ -45,4 +46,10 @@ defineProps<{ .main { margin: 1.5em 0 0 0; } + +.description { + font-size: 0.85em; + color: var(--fgTransparentWeak); + margin: 0 0 8px 0; +} </style> diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue index f65f8a78ff..af5daa10ff 100644 --- a/packages/frontend/src/components/form/suspense.vue +++ b/packages/frontend/src/components/form/suspense.vue @@ -21,7 +21,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, watch } from 'vue'; import MkButton from '@/components/MkButton.vue'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index 7689bba7bf..e2b59869a4 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -4,16 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu" v-on:click.stop> +<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu" @click.stop> <slot></slot> </a> </template> <script lang="ts" setup> +import { computed } from 'vue'; import * as os from '@/os.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { url } from '@/config.js'; -import { popout as popout_ } from '@/scripts/popout.js'; import { i18n } from '@/i18n.js'; import { useRouter } from '@/router.js'; @@ -28,7 +28,7 @@ const props = withDefaults(defineProps<{ const router = useRouter(); -const active = $computed(() => { +const active = computed(() => { if (props.activeClass == null) return false; const resolved = router.resolve(props.to); if (resolved == null) return false; @@ -56,11 +56,11 @@ function onContextmenu(ev) { action: () => { router.push(props.to, 'forcePage'); }, - }, null, { + }, { type: 'divider' }, { icon: 'ph-arrow-square-out ph-bold ph-lg', text: i18n.ts.openInNewTab, action: () => { - window.open(props.to, '_blank'); + window.open(props.to, '_blank', 'noopener'); }, }, { icon: 'ph-link ph-bold ph-lg', diff --git a/packages/frontend/src/components/global/MkAd.stories.impl.ts b/packages/frontend/src/components/global/MkAd.stories.impl.ts index 360bc88b4a..5ae45ec58f 100644 --- a/packages/frontend/src/components/global/MkAd.stories.impl.ts +++ b/packages/frontend/src/components/global/MkAd.stories.impl.ts @@ -4,11 +4,8 @@ */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { expect } from '@storybook/jest'; -import { userEvent, waitFor, within } from '@storybook/testing-library'; import { StoryObj } from '@storybook/vue3'; import MkAd from './MkAd.vue'; -import { i18n } from '@/i18n.js'; let lock: Promise<undefined> | undefined; diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index 3e092753a3..b3eb6d681f 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -96,7 +96,7 @@ const choseAd = (): Ad | null => { }; const chosen = ref(choseAd()); -const shouldHide = $ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null)); +const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null)); function reduceFrequency(): void { if (chosen.value == null) return; diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 01bf66fed5..4a876931c3 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -23,21 +23,24 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> </div> - <img - v-if="showDecoration && (decoration || user.avatarDecorations.length > 0)" - :class="[$style.decoration]" - :src="decoration?.url ?? user.avatarDecorations[0].url" - :style="{ - rotate: getDecorationAngle(), - scale: getDecorationScale(), - }" - alt="" - > + <template v-if="showDecoration"> + <img + v-for="decoration in decorations ?? user.avatarDecorations" + :class="[$style.decoration]" + :src="decoration.url" + :style="{ + rotate: getDecorationAngle(decoration), + scale: getDecorationScale(decoration), + translate: getDecorationOffset(decoration), + }" + alt="" + > + </template> </component> </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import MkImgWithBlurhash from '../MkImgWithBlurhash.vue'; import MkA from './MkA.vue'; @@ -47,9 +50,9 @@ import { acct, userPage } from '@/filters/user.js'; import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue'; import { defaultStore } from '@/store.js'; -const animation = $ref(defaultStore.state.animation); -const squareAvatars = $ref(defaultStore.state.squareAvatars); -const useBlurEffect = $ref(defaultStore.state.useBlurEffect); +const animation = ref(defaultStore.state.animation); +const squareAvatars = ref(defaultStore.state.squareAvatars); +const useBlurEffect = ref(defaultStore.state.useBlurEffect); const props = withDefaults(defineProps<{ user: Misskey.entities.User; @@ -57,19 +60,14 @@ const props = withDefaults(defineProps<{ link?: boolean; preview?: boolean; indicator?: boolean; - decoration?: { - url: string; - angle?: number; - flipH?: boolean; - flipV?: boolean; - }; + decorations?: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>[]; forceShowDecoration?: boolean; }>(), { target: null, link: false, preview: false, indicator: false, - decoration: undefined, + decorations: undefined, forceShowDecoration: false, }); @@ -79,11 +77,11 @@ const emit = defineEmits<{ const showDecoration = props.forceShowDecoration || defaultStore.state.showAvatarDecorations; -const bound = $computed(() => props.link +const bound = computed(() => props.link ? { to: userPage(props.user), target: props.target } : {}); -const url = $computed(() => (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.enableDataSaverMode) +const url = computed(() => (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) ? getStaticImageUrl(props.user.avatarUrl) : props.user.avatarUrl); @@ -92,34 +90,26 @@ function onClick(ev: MouseEvent): void { emit('click', ev); } -function getDecorationAngle() { - let angle; - if (props.decoration) { - angle = props.decoration.angle ?? 0; - } else if (props.user.avatarDecorations.length > 0) { - angle = props.user.avatarDecorations[0].angle ?? 0; - } else { - angle = 0; - } +function getDecorationAngle(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { + const angle = decoration.angle ?? 0; return angle === 0 ? undefined : `${angle * 360}deg`; } -function getDecorationScale() { - let scaleX; - if (props.decoration) { - scaleX = props.decoration.flipH ? -1 : 1; - } else if (props.user.avatarDecorations.length > 0) { - scaleX = props.user.avatarDecorations[0].flipH ? -1 : 1; - } else { - scaleX = 1; - } +function getDecorationScale(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { + const scaleX = decoration.flipH ? -1 : 1; return scaleX === 1 ? undefined : `${scaleX} 1`; } -let color = $ref<string | undefined>(); +function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { + const offsetX = decoration.offsetX ?? 0; + const offsetY = decoration.offsetY ?? 0; + return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`; +} + +const color = ref<string | undefined>(); watch(() => props.user.avatarBlurhash, () => { - color = extractAvgColorFromBlurhash(props.user.avatarBlurhash); + color.value = extractAvgColorFromBlurhash(props.user.avatarBlurhash); }, { immediate: true, }); diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 10d7d93b01..e8732d1b16 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -19,12 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, inject } from 'vue'; +import { computed, inject, ref } from 'vue'; import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js'; import { defaultStore } from '@/store.js'; import { customEmojisMap } from '@/custom-emojis.js'; import * as os from '@/os.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import * as sound from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -71,7 +72,7 @@ const url = computed(() => { }); const alt = computed(() => `:${customEmojiName.value}:`); -let errored = $ref(url.value == null); +const errored = ref(url.value == null); function onClick(ev: MouseEvent) { if (props.menu) { @@ -90,6 +91,7 @@ function onClick(ev: MouseEvent) { icon: 'ph-smiley ph-bold ph-lg', action: () => { react(`:${props.name}:`); + sound.play('reaction'); }, }] : [])], ev.currentTarget ?? ev.target); } diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index d5025edf82..b1d62db33c 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -16,6 +16,7 @@ import { defaultStore } from '@/store.js'; import { 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'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -56,6 +57,7 @@ function onClick(ev: MouseEvent) { icon: 'ph-smiley ph-bold ph-lg', action: () => { react(props.emoji); + sound.play('reaction'); }, }] : [])], ev.currentTarget ?? ev.target); } diff --git a/packages/frontend/src/components/global/MkLazy.vue b/packages/frontend/src/components/global/MkLazy.vue new file mode 100644 index 0000000000..6d7ff4ca49 --- /dev/null +++ b/packages/frontend/src/components/global/MkLazy.vue @@ -0,0 +1,53 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div ref="rootEl" :class="$style.root"> + <div v-if="!showing" :class="$style.placeholder"></div> + <slot v-else></slot> +</div> +</template> + +<script lang="ts" setup> +import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, shallowRef } from 'vue'; + +const rootEl = shallowRef<HTMLDivElement>(); +const showing = ref(false); + +const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + showing.value = true; + } + }, +); + +onMounted(() => { + nextTick(() => { + observer.observe(rootEl.value!); + }); +}); + +onActivated(() => { + nextTick(() => { + observer.observe(rootEl.value!); + }); +}); + +onBeforeUnmount(() => { + observer.disconnect(); +}); +</script> + +<style lang="scss" module> +.root { + display: block; +} + +.placeholder { + display: block; + min-height: 150px; +} +</style> diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index bd6a599a98..60d12fdcde 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -37,7 +37,7 @@ type MfmProps = { isNote?: boolean; emojiUrls?: string[]; rootScale?: number; - nyaize: boolean | 'respect'; + nyaize?: boolean | 'respect'; parsedNodes?: mfm.MfmNode[] | null; enableEmojiMenu?: boolean; enableEmojiMenuReaction?: boolean; @@ -110,26 +110,30 @@ export default function(props: MfmProps) { case 'fn': { // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる - let style; + let style: string | undefined; switch (token.props.name) { case 'tada': { const speed = validTime(token.props.args.speed) ?? '1s'; - style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : ''); + const delay = validTime(token.props.args.delay) ?? '0s'; + style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both; animation-delay: ${delay};` : ''); break; } case 'jelly': { const speed = validTime(token.props.args.speed) ?? '1s'; - style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : ''); + const delay = validTime(token.props.args.delay) ?? '0s'; + style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both; animation-delay: ${delay};` : ''); break; } case 'twitch': { const speed = validTime(token.props.args.speed) ?? '0.5s'; - style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : ''; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-twitch ${speed} ease infinite; animation-delay: ${delay};` : ''; break; } case 'shake': { const speed = validTime(token.props.args.speed) ?? '0.5s'; - style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : ''; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-shake ${speed} ease infinite; animation-delay: ${delay};` : ''; break; } case 'spin': { @@ -142,17 +146,20 @@ export default function(props: MfmProps) { token.props.args.y ? 'mfm-spinY' : 'mfm-spin'; const speed = validTime(token.props.args.speed) ?? '1.5s'; - style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : ''; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction}; animation-delay: ${delay};` : ''; break; } case 'jump': { const speed = validTime(token.props.args.speed) ?? '0.75s'; - style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : ''; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-jump ${speed} linear infinite; animation-delay: ${delay};` : ''; break; } case 'bounce': { const speed = validTime(token.props.args.speed) ?? '0.75s'; - style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : ''; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom; animation-delay: ${delay};` : ''; break; } case 'flip': { @@ -202,7 +209,8 @@ export default function(props: MfmProps) { }, genEl(token.children, scale)); } const speed = validTime(token.props.args.speed) ?? '1s'; - style = `animation: mfm-rainbow ${speed} linear infinite;`; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`; break; } case 'sparkle': { @@ -249,11 +257,17 @@ export default function(props: MfmProps) { case 'ruby': { if (token.children.length === 1) { const child = token.children[0]; - const text = child.type === 'text' ? child.props.text : ''; + let text = child.type === 'text' ? child.props.text : ''; + if (!disableNyaize && shouldNyaize) { + text = doNyaize(text); + } return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]); } else { const rt = token.children.at(-1)!; - const text = rt.type === 'text' ? rt.props.text : ''; + let text = rt.type === 'text' ? rt.props.text : ''; + if (!disableNyaize && shouldNyaize) { + text = doNyaize(text); + } return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]); } } @@ -275,7 +289,7 @@ export default function(props: MfmProps) { ]); } } - if (style == null) { + if (style === undefined) { return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']); } else { return h('span', { diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index fd7aec5e5a..a36d9517cd 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -50,23 +50,19 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted, ref, inject } from 'vue'; +import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue'; import tinycolor from 'tinycolor2'; import XTabs, { Tab } from './MkPageHeader.tabs.vue'; import { scrollToTop } from '@/scripts/scroll.js'; import { globalEvents } from '@/events.js'; import { injectPageMetadata } from '@/scripts/page-metadata.js'; import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; +import { PageHeaderItem } from '@/types/page-header.js'; const props = withDefaults(defineProps<{ tabs?: Tab[]; tab?: string; - actions?: { - text: string; - icon: string; - highlighted?: boolean; - handler: (ev: MouseEvent) => void; - }[]; + actions?: PageHeaderItem[] | null; thin?: boolean; displayMyAvatar?: boolean; displayBackButton?: boolean; @@ -85,13 +81,13 @@ const metadata = injectPageMetadata(); const hideTitle = inject('shouldOmitHeaderTitle', false); const thin_ = props.thin || inject('shouldHeaderThin', false); -let el = $shallowRef<HTMLElement | undefined>(undefined); +const el = shallowRef<HTMLElement | undefined>(undefined); const bg = ref<string | undefined>(undefined); -let narrow = $ref(false); -const hasTabs = $computed(() => props.tabs.length > 0); -const hasActions = $computed(() => props.actions && props.actions.length > 0); -const show = $computed(() => { - return !hideTitle || hasTabs || hasActions; +const narrow = ref(false); +const hasTabs = computed(() => props.tabs.length > 0); +const hasActions = computed(() => props.actions && props.actions.length > 0); +const show = computed(() => { + return !hideTitle || hasTabs.value || hasActions.value; }); const preventDrag = (ev: TouchEvent) => { @@ -99,8 +95,8 @@ const preventDrag = (ev: TouchEvent) => { }; const top = () => { - if (el) { - scrollToTop(el as HTMLElement, { behavior: 'smooth' }); + if (el.value) { + scrollToTop(el.value as HTMLElement, { behavior: 'smooth' }); } }; @@ -131,14 +127,14 @@ onMounted(() => { calcBg(); globalEvents.on('themeChanged', calcBg); - if (el && el.parentElement) { - narrow = el.parentElement.offsetWidth < 500; + if (el.value && el.value.parentElement) { + narrow.value = el.value.parentElement.offsetWidth < 500; ro = new ResizeObserver((entries, observer) => { - if (el && el.parentElement && document.body.contains(el as HTMLElement)) { - narrow = el.parentElement.offsetWidth < 500; + if (el.value && el.value.parentElement && document.body.contains(el.value as HTMLElement)) { + narrow.value = el.value.parentElement.offsetWidth < 500; } }); - ro.observe(el.parentElement as HTMLElement); + ro.observe(el.value.parentElement as HTMLElement); } }); diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue index 8e9bff11d1..1d707af2d1 100644 --- a/packages/frontend/src/components/global/MkStickyContainer.vue +++ b/packages/frontend/src/components/global/MkStickyContainer.vue @@ -18,36 +18,36 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue'; -import { $$ } from 'vue/macros'; +import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue'; + import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const'; -const rootEl = $shallowRef<HTMLElement>(); -const headerEl = $shallowRef<HTMLElement>(); -const footerEl = $shallowRef<HTMLElement>(); -const bodyEl = $shallowRef<HTMLElement>(); +const rootEl = shallowRef<HTMLElement>(); +const headerEl = shallowRef<HTMLElement>(); +const footerEl = shallowRef<HTMLElement>(); +const bodyEl = shallowRef<HTMLElement>(); -let headerHeight = $ref<string | undefined>(); -let childStickyTop = $ref(0); +const headerHeight = ref<string | undefined>(); +const childStickyTop = ref(0); const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0)); -provide(CURRENT_STICKY_TOP, $$(childStickyTop)); +provide(CURRENT_STICKY_TOP, childStickyTop); -let footerHeight = $ref<string | undefined>(); -let childStickyBottom = $ref(0); +const footerHeight = ref<string | undefined>(); +const childStickyBottom = ref(0); const parentStickyBottom = inject<Ref<number>>(CURRENT_STICKY_BOTTOM, ref(0)); -provide(CURRENT_STICKY_BOTTOM, $$(childStickyBottom)); +provide(CURRENT_STICKY_BOTTOM, childStickyBottom); const calc = () => { // コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる - if (headerEl != null) { - childStickyTop = parentStickyTop.value + headerEl.offsetHeight; - headerHeight = headerEl.offsetHeight.toString(); + if (headerEl.value != null) { + childStickyTop.value = parentStickyTop.value + headerEl.value.offsetHeight; + headerHeight.value = headerEl.value.offsetHeight.toString(); } // コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる - if (footerEl != null) { - childStickyBottom = parentStickyBottom.value + footerEl.offsetHeight; - footerHeight = footerEl.offsetHeight.toString(); + if (footerEl.value != null) { + childStickyBottom.value = parentStickyBottom.value + footerEl.value.offsetHeight; + footerHeight.value = footerEl.value.offsetHeight.toString(); } }; @@ -62,28 +62,28 @@ onMounted(() => { watch([parentStickyTop, parentStickyBottom], calc); - watch($$(childStickyTop), () => { - bodyEl.style.setProperty('--stickyTop', `${childStickyTop}px`); + watch(childStickyTop, () => { + bodyEl.value.style.setProperty('--stickyTop', `${childStickyTop.value}px`); }, { immediate: true, }); - watch($$(childStickyBottom), () => { - bodyEl.style.setProperty('--stickyBottom', `${childStickyBottom}px`); + watch(childStickyBottom, () => { + bodyEl.value.style.setProperty('--stickyBottom', `${childStickyBottom.value}px`); }, { immediate: true, }); - headerEl.style.position = 'sticky'; - headerEl.style.top = 'var(--stickyTop, 0)'; - headerEl.style.zIndex = '1000'; + headerEl.value.style.position = 'sticky'; + headerEl.value.style.top = 'var(--stickyTop, 0)'; + headerEl.value.style.zIndex = '1000'; - footerEl.style.position = 'sticky'; - footerEl.style.bottom = 'var(--stickyBottom, 0)'; - footerEl.style.zIndex = '1000'; + footerEl.value.style.position = 'sticky'; + footerEl.value.style.bottom = 'var(--stickyBottom, 0)'; + footerEl.value.style.zIndex = '1000'; - observer.observe(headerEl); - observer.observe(footerEl); + observer.observe(headerEl.value); + observer.observe(footerEl.value); }); onUnmounted(() => { @@ -91,6 +91,6 @@ onUnmounted(() => { }); defineExpose({ - rootEl: $$(rootEl), + rootEl: rootEl, }); </script> diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index f08d538fc0..e11db9dc31 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import isChromatic from 'chromatic/isChromatic'; -import { onMounted, onUnmounted } from 'vue'; +import { onMounted, onUnmounted, ref, computed } from 'vue'; import { i18n } from '@/i18n.js'; import { dateTimeFormat } from '@/scripts/intl-const.js'; @@ -28,35 +28,48 @@ const props = withDefaults(defineProps<{ mode: 'relative', }); -const _time = props.time == null ? NaN : - typeof props.time === 'number' ? props.time : - (props.time instanceof Date ? props.time : new Date(props.time)).getTime(); +function getDateSafe(n: Date | string | number) { + try { + if (n instanceof Date) { + return n; + } + return new Date(n); + } catch (err) { + return { + getTime: () => NaN, + }; + } +} + +// eslint-disable-next-line vue/no-setup-props-destructure +const _time = props.time == null ? NaN : getDateSafe(props.time).getTime(); const invalid = Number.isNaN(_time); const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid; -let now = $ref((props.origin ?? new Date()).getTime()); -const ago = $computed(() => (now - _time) / 1000/*ms*/); +// eslint-disable-next-line vue/no-setup-props-destructure +const now = ref((props.origin ?? new Date()).getTime()); +const ago = computed(() => (now.value - _time) / 1000/*ms*/); -const relative = $computed<string>(() => { +const relative = computed<string>(() => { if (props.mode === 'absolute') return ''; // absoluteではrelativeを使わないので計算しない if (invalid) return i18n.ts._ago.invalid; return ( - ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago / 31536000).toString() }) : - ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago / 2592000).toString() }) : - ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago / 604800).toString() }) : - ago >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago / 86400).toString() }) : - ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() }) : - ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : - ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : - ago >= -3 ? i18n.ts._ago.justNow : - ago < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago / 31536000).toString() }) : - ago < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago / 2592000).toString() }) : - ago < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago / 604800).toString() }) : - ago < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago / 86400).toString() }) : - ago < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago / 3600).toString() }) : - ago < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago / 60)).toString() }) : - i18n.t('_timeIn.seconds', { n: (~~(-ago % 60)).toString() }) + ago.value >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago.value / 31536000).toString() }) : + ago.value >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago.value / 2592000).toString() }) : + ago.value >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago.value / 604800).toString() }) : + ago.value >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago.value / 86400).toString() }) : + ago.value >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago.value / 3600).toString() }) : + ago.value >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago.value / 60)).toString() }) : + ago.value >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago.value % 60)).toString() }) : + ago.value >= -3 ? i18n.ts._ago.justNow : + ago.value < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago.value / 31536000).toString() }) : + ago.value < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago.value / 2592000).toString() }) : + ago.value < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago.value / 604800).toString() }) : + ago.value < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago.value / 86400).toString() }) : + ago.value < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago.value / 3600).toString() }) : + ago.value < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago.value / 60)).toString() }) : + i18n.t('_timeIn.seconds', { n: (~~(-ago.value % 60)).toString() }) ); }); @@ -64,8 +77,8 @@ let tickId: number; let currentInterval: number; function tick() { - now = (new Date()).getTime(); - const nextInterval = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000; + now.value = (new Date()).getTime(); + const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000; if (currentInterval !== nextInterval) { if (tickId) window.clearInterval(tickId); diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index d29c720278..667a113432 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <component - :is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel" :target="target" + :is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel ?? 'nofollow noopener'" :target="target" @contextmenu.stop="() => {}" > <template v-if="!self"> diff --git a/packages/frontend/src/components/global/MkUserName.stories.impl.ts b/packages/frontend/src/components/global/MkUserName.stories.impl.ts index 8c24a4819f..01455e492d 100644 --- a/packages/frontend/src/components/global/MkUserName.stories.impl.ts +++ b/packages/frontend/src/components/global/MkUserName.stories.impl.ts @@ -5,7 +5,6 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { expect } from '@storybook/jest'; -import { userEvent, within } from '@storybook/testing-library'; import { StoryObj } from '@storybook/vue3'; import { userDetailed } from '../../../.storybook/fakes'; import MkUserName from './MkUserName.vue'; diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index 99f42f4fcb..9da8f8c379 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, onBeforeUnmount, provide } from 'vue'; +import { inject, onBeforeUnmount, provide, shallowRef, ref } from 'vue'; import { Resolved, Router } from '@/nirax'; import { defaultStore } from '@/store.js'; @@ -46,16 +46,16 @@ function resolveNested(current: Resolved, d = 0): Resolved | null { } const current = resolveNested(router.current)!; -let currentPageComponent = $shallowRef(current.route.component); -let currentPageProps = $ref(current.props); -let key = $ref(current.route.path + JSON.stringify(Object.fromEntries(current.props))); +const currentPageComponent = shallowRef(current.route.component); +const currentPageProps = ref(current.props); +const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props))); function onChange({ resolved, key: newKey }) { const current = resolveNested(resolved); if (current == null) return; - currentPageComponent = current.route.component; - currentPageProps = current.props; - key = current.route.path + JSON.stringify(Object.fromEntries(current.props)); + currentPageComponent.value = current.route.component; + currentPageProps.value = current.props; + key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props)); } router.addListener('change', onChange); diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index c740d181f9..a3e13c3a50 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -25,6 +25,7 @@ import MkPageHeader from './global/MkPageHeader.vue'; import MkSpacer from './global/MkSpacer.vue'; import MkFooterSpacer from './global/MkFooterSpacer.vue'; import MkStickyContainer from './global/MkStickyContainer.vue'; +import MkLazy from './global/MkLazy.vue'; export default function(app: App) { for (const [key, value] of Object.entries(components)) { @@ -53,6 +54,7 @@ export const components = { MkSpacer: MkSpacer, MkFooterSpacer: MkFooterSpacer, MkStickyContainer: MkStickyContainer, + MkLazy: MkLazy, }; declare module '@vue/runtime-core' { @@ -77,5 +79,6 @@ declare module '@vue/runtime-core' { MkSpacer: typeof MkSpacer; MkFooterSpacer: typeof MkFooterSpacer; MkStickyContainer: typeof MkStickyContainer; + MkLazy: typeof MkLazy; } } diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue index 6aa2c1c0b7..892522d4b5 100644 --- a/packages/frontend/src/components/page/page.text.vue +++ b/packages/frontend/src/components/page/page.text.vue @@ -16,7 +16,6 @@ import * as mfm from '@sharkey/sfm-js'; import * as Misskey from 'misskey-js'; import { TextBlock } from './block.type'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; -import { $i } from '@/account.js'; const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue')); diff --git a/packages/frontend/src/components/page/page.vue b/packages/frontend/src/components/page/page.vue index ab37ca69ad..94ca7bdf04 100644 --- a/packages/frontend/src/components/page/page.vue +++ b/packages/frontend/src/components/page/page.vue @@ -10,7 +10,6 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, nextTick } from 'vue'; import * as Misskey from 'misskey-js'; import XBlock from './page.block.vue'; diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 2f8d57c7f6..cdd6731269 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -105,7 +105,21 @@ https://github.com/sindresorhus/file-type/blob/main/core.js https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers */ -export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const; +export const notificationTypes = [ + 'note', + 'follow', + 'mention', + 'reply', + 'renote', + 'quote', + 'reaction', + 'pollEnded', + 'receiveFollowRequest', + 'followRequestAccepted', + 'roleAssigned', + 'achievementEarned', + 'app', +] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; export const ROLE_POLICIES = [ @@ -134,6 +148,7 @@ export const ROLE_POLICIES = [ 'userListLimit', 'userEachUserListsLimit', 'rateLimitFactor', + 'avatarDecorationLimit', ] as const; // なんか動かない diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts index 8ecd1bd2eb..6a48159f13 100644 --- a/packages/frontend/src/custom-emojis.ts +++ b/packages/frontend/src/custom-emojis.ts @@ -10,7 +10,7 @@ import { useStream } from '@/stream.js'; import { get, set } from '@/scripts/idb-proxy.js'; const storageCache = await get('emojis'); -export const customEmojis = shallowRef<Misskey.entities.CustomEmoji[]>(Array.isArray(storageCache) ? storageCache : []); +export const customEmojis = shallowRef<Misskey.entities.EmojiSimple[]>(Array.isArray(storageCache) ? storageCache : []); export const customEmojiCategories = computed<[ ...string[], null ]>(() => { const categories = new Set<string>(); for (const emoji of customEmojis.value) { @@ -21,7 +21,7 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => { return markRaw([...Array.from(categories), null]); }); -export const customEmojisMap = new Map<string, Misskey.entities.CustomEmoji>(); +export const customEmojisMap = new Map<string, Misskey.entities.EmojiSimple>(); watch(customEmojis, emojis => { customEmojisMap.clear(); for (const emoji of emojis) { @@ -38,7 +38,7 @@ stream.on('emojiAdded', emojiData => { }); stream.on('emojiUpdated', emojiData => { - customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.CustomEmoji ?? item); + customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.EmojiSimple ?? item); set('emojis', customEmojis.value); }); diff --git a/packages/frontend/src/emojilist.json b/packages/frontend/src/emojilist.json index fe1d884ebe..75d5c34d71 100644 --- a/packages/frontend/src/emojilist.json +++ b/packages/frontend/src/emojilist.json @@ -103,6 +103,7 @@ ["🫥", "dotted_line_face", 0], ["🫤", "face_with_diagonal_mouth", 0], ["🥹", "face_holding_back_tears", 0], + ["🫨", "shaking_face", 0], ["💩", "poop", 0], ["😈", "smiling_imp", 0], ["👿", "imp", 0], @@ -132,6 +133,8 @@ ["✊", "fist", 1], ["🤛", "fist_left", 1], ["🤜", "fist_right", 1], + ["🫷", "leftwards_pushing_hand", 1], + ["🫸", "rightwards_pushing_hand", 1], ["✌", "v", 1], ["👌", "ok_hand", 1], ["✋", "raised_hand", 1], @@ -453,6 +456,7 @@ ["🐸", "frog", 2], ["🦑", "squid", 2], ["🐙", "octopus", 2], + ["🪼", "jellyfish", 2], ["🦐", "shrimp", 2], ["🐵", "monkey_face", 2], ["🦍", "gorilla", 2], @@ -466,7 +470,9 @@ ["🐤", "baby_chick", 2], ["🐣", "hatching_chick", 2], ["🐥", "hatched_chick", 2], + ["🪿", "goose", 2], ["🦆", "duck", 2], + ["🐦⬛", "black_bird", 2], ["🦅", "eagle", 2], ["🦉", "owl", 2], ["🦇", "bat", 2], @@ -474,6 +480,7 @@ ["🐗", "boar", 2], ["🐴", "horse", 2], ["🦄", "unicorn", 2], + ["🫎", "moose", 2], ["🐝", "honeybee", 2], ["🐛", "bug", 2], ["🦋", "butterfly", 2], @@ -516,6 +523,7 @@ ["🐐", "goat", 2], ["🐏", "ram", 2], ["🐑", "sheep", 2], + ["🫏", "donkey", 2], ["🐎", "racehorse", 2], ["🐖", "pig2", 2], ["🐀", "rat", 2], @@ -546,6 +554,7 @@ ["🐻❄️", "polar_bear", 2], ["🦤", "dodo", 2], ["🪶", "feather", 2], + ["🪽", "wing", 2], ["🦭", "seal", 2], ["🐾", "paw_prints", 2], ["🐉", "dragon", 2], @@ -576,6 +585,7 @@ ["🌻", "sunflower", 2], ["🌹", "rose", 2], ["🥀", "wilted_flower", 2], + ["🪻", "hyacinth", 2], ["🌷", "tulip", 2], ["🌼", "blossom", 2], ["🌸", "cherry_blossom", 2], @@ -655,6 +665,7 @@ ["🥝", "kiwi_fruit", 3], ["🥭", "mango", 3], ["🥑", "avocado", 3], + ["🫛", "pea_pod", 3], ["🥦", "broccoli", 3], ["🍅", "tomato", 3], ["🍆", "eggplant", 3], @@ -668,6 +679,7 @@ ["🌽", "corn", 3], ["🥬", "leafy_greens", 3], ["🍠", "sweet_potato", 3], + ["🫚", "ginger_root", 3], ["🥜", "peanuts", 3], ["🧄", "garlic", 3], ["🧅", "onion", 3], @@ -850,9 +862,11 @@ ["🎧", "headphones", 4], ["🎼", "musical_score", 4], ["🎹", "musical_keyboard", 4], + ["🪇", "maracas", 4], ["🥁", "drum", 4], ["🎷", "saxophone", 4], ["🎺", "trumpet", 4], + ["🪈", "flute", 4], ["🎸", "guitar", 4], ["🎻", "violin", 4], ["🪕", "banjo", 4], @@ -1108,6 +1122,7 @@ ["🩹", "adhesive_bandage", 6], ["🩺", "stethoscope", 6], ["🪒", "razor", 6], + ["🪮", "hair_pick", 6], ["🩻", "xray", 6], ["🩼", "crutch", 6], ["🧬", "dna", 6], @@ -1156,6 +1171,7 @@ ["🎊", "confetti_ball", 6], ["🎉", "tada", 6], ["🎎", "dolls", 6], + ["🪭", "folding_hand_fan", 6], ["🎐", "wind_chime", 6], ["🎌", "crossed_flags", 6], ["🏮", "izakaya_lantern", 6], @@ -1237,14 +1253,17 @@ ["🪧", "placard", 6], ["💯", "100", 7], ["🔢", "1234", 7], + ["🩷", "pink_heart", 7], ["❤️", "heart", 7], ["🧡", "orange_heart", 7], ["💛", "yellow_heart", 7], ["💚", "green_heart", 7], + ["🩵", "light_blue_heart", 7], ["💙", "blue_heart", 7], ["💜", "purple_heart", 7], ["🤎", "brown_heart", 7], ["🖤", "black_heart", 7], + ["🩶", "grey_heart", 7], ["🤍", "white_heart", 7], ["💔", "broken_heart", 7], ["❣", "heavy_heart_exclamation", 7], @@ -1263,6 +1282,7 @@ ["☪", "star_and_crescent", 7], ["🕉", "om", 7], ["☸", "wheel_of_dharma", 7], + ["🪯", "khanda", 7], ["✡", "star_of_david", 7], ["🔯", "six_pointed_star", 7], ["🕎", "menorah", 7], @@ -1358,6 +1378,7 @@ ["🛃", "customs", 7], ["🛄", "baggage_claim", 7], ["🛅", "left_luggage", 7], + ["🛜", "wireless", 7], ["♿", "wheelchair", 7], ["🚭", "no_smoking", 7], ["🚾", "wc", 7], diff --git a/packages/frontend/src/filters/user.ts b/packages/frontend/src/filters/user.ts index a7a13bec6e..8d20603725 100644 --- a/packages/frontend/src/filters/user.ts +++ b/packages/frontend/src/filters/user.ts @@ -4,7 +4,6 @@ */ import * as Misskey from 'misskey-js'; -import * as Misskey from 'misskey-js'; import { url } from '@/config.js'; export const acct = (user: misskey.Acct) => { diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html new file mode 100644 index 0000000000..8de01e4802 --- /dev/null +++ b/packages/frontend/src/index.html @@ -0,0 +1,35 @@ +<!-- + SPDX-FileCopyrightText: syuilo and other misskey contributors + SPDX-License-Identifier: AGPL-3.0-only +--> + +<!-- + 開発モードのviteはこのファイルを起点にサーバーを起動します。 + このファイルに書かれた [t]js のリンクと (s)cssのリンクと、その依存関係にあるファイルはビルドされます +--> + +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8" /> + <title>[DEV] Loading...</title> + <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> + <meta + http-equiv="Content-Security-Policy" + content="default-src 'self'; + worker-src 'self'; + script-src 'self' 'unsafe-eval'; + 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; + 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;" + /> + <meta property="og:site_name" content="[DEV BUILD] Misskey" /> + <meta name="viewport" content="width=device-width, initial-scale=1"> +</head> + +<body> +<div id="misskey_app"></div> +<script type="module" src="./_dev_boot_.ts"></script> +</body> +</html> diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts index cbfd95951c..b09264dabb 100644 --- a/packages/frontend/src/instance.ts +++ b/packages/frontend/src/instance.ts @@ -15,7 +15,7 @@ const cached = miLocalStorage.getItem('instance'); // TODO: instanceをリアクティブにするかは再考の余地あり -export const instance: Misskey.entities.InstanceMetadata = reactive(cached ? JSON.parse(cached) : { +export const instance: Misskey.entities.MetaResponse = reactive(cached ? JSON.parse(cached) : { // TODO: set default values }); diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 3023471b14..d95ff2119c 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -36,7 +36,8 @@ type Keys = `themes:${string}` | `aiscript:${string}` | 'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~) - 'emojis' // DEPRECATED, stored in indexeddb (13.9.0~); + 'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~); + `channelLastReadedAt:${string}` export const miLocalStorage = { getItem: (key: Keys): string | null => window.localStorage.getItem(key), diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index 11a0feea26..d615c751ee 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -4,6 +4,8 @@ */ import { computed, reactive } from 'vue'; +import { clearCache } from './scripts/clear-cache.js'; +import { instance } from './instance.js'; import { $i } from '@/account.js'; import { miLocalStorage } from '@/local-storage.js'; import { openInstanceMenu } from '@/ui/_common_/common.js'; @@ -12,7 +14,6 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { ui } from '@/config.js'; import { unisonReload } from '@/scripts/unison-reload.js'; -import { instance } from './instance.js'; export const navbarItemDef = reactive({ notifications: { @@ -165,4 +166,11 @@ export const navbarItemDef = reactive({ show: computed(() => $i != null), to: `/@${$i?.username}`, }, + cacheClear: { + title: i18n.ts.clearCache, + icon: 'ph-trash ph-bold ph-lg', + action: (ev) => { + clearCache(); + }, + }, }); diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 8093335a28..b02f6aa640 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -5,8 +5,8 @@ // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する -import { pendingApiRequestsCount, api, apiExternal, apiGet } from '@/scripts/api.js'; -export { pendingApiRequestsCount, api, apiExternal, apiGet }; +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'; @@ -546,7 +546,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: }); } -export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement, options?: { +export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement | EventTarget | null, options?: { align?: string; width?: number; viaKeyboard?: boolean; diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue index fe27206937..2cdf8f2e8c 100644 --- a/packages/frontend/src/pages/_error_.vue +++ b/packages/frontend/src/pages/_error_.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import { version } from '@/config.js'; @@ -42,29 +42,29 @@ const props = withDefaults(defineProps<{ }>(), { }); -let loaded = $ref(false); -let serverIsDead = $ref(false); -let meta = $ref<Misskey.entities.LiteInstanceMetadata | null>(null); +const loaded = ref(false); +const serverIsDead = ref(false); +const meta = ref<Misskey.entities.MetaResponse | null>(null); os.api('meta', { detail: false, }).then(res => { - loaded = true; - serverIsDead = false; - meta = res; + loaded.value = true; + serverIsDead.value = false; + meta.value = res; miLocalStorage.setItem('v', res.version); }, () => { - loaded = true; - serverIsDead = true; + loaded.value = true; + serverIsDead.value = true; }); function reload() { unisonReload(); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.error, diff --git a/packages/frontend/src/pages/about-sharkey.vue b/packages/frontend/src/pages/about-sharkey.vue index e66aab2098..4160dcb09c 100644 --- a/packages/frontend/src/pages/about-sharkey.vue +++ b/packages/frontend/src/pages/about-sharkey.vue @@ -74,9 +74,9 @@ SPDX-License-Identifier: AGPL-3.0-only <img src="https://avatars.githubusercontent.com/u/67428053?v=4" :class="$style.contributorAvatar"> <span :class="$style.contributorUsername">@kakkokari-gtyih</span> </a> - <a href="https://github.com/taichanNE30" target="_blank" :class="$style.contributor"> + <a href="https://github.com/tai-cha" target="_blank" :class="$style.contributor"> <img src="https://avatars.githubusercontent.com/u/40626578?v=4" :class="$style.contributorAvatar"> - <span :class="$style.contributorUsername">@taichanNE30</span> + <span :class="$style.contributorUsername">@tai-cha</span> </a> </div> </FormSection> @@ -102,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, onBeforeUnmount } from 'vue'; +import { nextTick, onBeforeUnmount, ref, shallowRef, computed } from 'vue'; import { version } from '@/config.js'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; @@ -116,21 +116,21 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; import { $i } from '@/account.js'; -let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure')); +const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure')); let easterEggReady = false; -let easterEggEmojis = $ref([]); -let easterEggEngine = $ref(null); -let sponsors = $ref([]); -const containerEl = $shallowRef<HTMLElement>(); +const easterEggEmojis = ref([]); +const easterEggEngine = ref(null); +const sponsors = ref([]); +const containerEl = shallowRef<HTMLElement>(); -await os.api('sponsors', { forceUpdate: true }).then((res) => sponsors.push(res.sponsor_data)); +await os.api('sponsors', { forceUpdate: true }).then((res) => sponsors.value.push(res.sponsor_data)); function iconLoaded() { const emojis = defaultStore.state.reactions; - const containerWidth = containerEl.offsetWidth; + const containerWidth = containerEl.value.offsetWidth; for (let i = 0; i < 32; i++) { - easterEggEmojis.push({ + easterEggEmojis.value.push({ id: i.toString(), top: -(128 + (Math.random() * 256)), left: (Math.random() * containerWidth), @@ -146,30 +146,30 @@ function iconLoaded() { function gravity() { if (!easterEggReady) return; easterEggReady = false; - easterEggEngine = physics(containerEl); + easterEggEngine.value = physics(containerEl.value); } function iLoveMisskey() { os.post({ - initialText: 'I $[jelly ❤] #Sharkey', + initialText: 'I $[jelly ❤] #Misskey', instant: true, }); } function getTreasure() { - thereIsTreasure = false; + thereIsTreasure.value = false; claimAchievement('foundTreasure'); } onBeforeUnmount(() => { - if (easterEggEngine) { - easterEggEngine.stop(); + if (easterEggEngine.value) { + easterEggEngine.value.stop(); } }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.aboutMisskey, diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue index 867b305de0..eda6455fd6 100644 --- a/packages/frontend/src/pages/about.emojis.vue +++ b/packages/frontend/src/pages/about.emojis.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XEmoji from './emojis.emoji.vue'; import MkButton from '@/components/MkButton.vue'; @@ -47,44 +47,44 @@ import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; const customEmojiTags = getCustomEmojiTags(); -let q = $ref(''); -let searchEmojis = $ref<Misskey.entities.CustomEmoji[]>(null); -let selectedTags = $ref(new Set()); +const q = ref(''); +const searchEmojis = ref<Misskey.entities.EmojiSimple[]>(null); +const selectedTags = ref(new Set()); function search() { - if ((q === '' || q == null) && selectedTags.size === 0) { - searchEmojis = null; + if ((q.value === '' || q.value == null) && selectedTags.value.size === 0) { + searchEmojis.value = null; return; } - if (selectedTags.size === 0) { - const queryarry = q.match(/\:([a-z0-9_]*)\:/g); + if (selectedTags.value.size === 0) { + const queryarry = q.value.match(/\:([a-z0-9_]*)\:/g); if (queryarry) { - searchEmojis = customEmojis.value.filter(emoji => + searchEmojis.value = customEmojis.value.filter(emoji => queryarry.includes(`:${emoji.name}:`), ); } else { - searchEmojis = customEmojis.value.filter(emoji => emoji.name.includes(q) || emoji.aliases.includes(q)); + searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value)); } } else { - searchEmojis = customEmojis.value.filter(emoji => (emoji.name.includes(q) || emoji.aliases.includes(q)) && [...selectedTags].every(t => emoji.aliases.includes(t))); + searchEmojis.value = customEmojis.value.filter(emoji => (emoji.name.includes(q.value) || emoji.aliases.includes(q.value)) && [...selectedTags.value].every(t => emoji.aliases.includes(t))); } } function toggleTag(tag) { - if (selectedTags.has(tag)) { - selectedTags.delete(tag); + if (selectedTags.value.has(tag)) { + selectedTags.value.delete(tag); } else { - selectedTags.add(tag); + selectedTags.value.add(tag); } } -watch($$(q), () => { +watch(q, () => { search(); }); -watch($$(selectedTags), () => { +watch(selectedTags, () => { search(); }, { deep: true }); </script> diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index 27f7784007..0de000ee3e 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue'; @@ -60,26 +60,26 @@ import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue'; import FormSplit from '@/components/form/split.vue'; import { i18n } from '@/i18n.js'; -let host = $ref(''); -let state = $ref('federating'); -let sort = $ref('+pubSub'); +const host = ref(''); +const state = ref('federating'); +const sort = ref('+pubSub'); const pagination = { endpoint: 'federation/instances' as const, limit: 10, displayLimit: 50, offsetMode: true, params: computed(() => ({ - sort: sort, - host: host !== '' ? host : null, + sort: sort.value, + host: host.value !== '' ? host.value : null, ...( - state === 'federating' ? { federating: true } : - state === 'subscribing' ? { subscribing: true } : - state === 'publishing' ? { publishing: true } : - state === 'suspended' ? { suspended: true } : - state === 'blocked' ? { blocked: true } : - state === 'silenced' ? { silenced: true } : - state === 'notResponding' ? { notResponding: true } : - state === 'nsfw' ? { nsfw: true } : + state.value === 'federating' ? { federating: true } : + state.value === 'subscribing' ? { subscribing: true } : + state.value === 'publishing' ? { publishing: true } : + state.value === 'suspended' ? { suspended: true } : + state.value === 'blocked' ? { blocked: true } : + state.value === 'silenced' ? { silenced: true } : + state.value === 'notResponding' ? { notResponding: true } : + state.value === 'nsfw' ? { nsfw: true } : {}), })), } as Paging; diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index e3586ba587..ff6ed2a624 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -102,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref } from 'vue'; import XEmojis from './about.emojis.vue'; import XFederation from './about.federation.vue'; import { version, host } from '@/config.js'; @@ -126,23 +126,23 @@ const props = withDefaults(defineProps<{ initialTab: 'overview', }); -let stats = $ref(null); -let tab = $ref(props.initialTab); +const stats = ref(null); +const tab = ref(props.initialTab); -watch($$(tab), () => { - if (tab === 'charts') { +watch(tab, () => { + if (tab.value === 'charts') { claimAchievement('viewInstanceChart'); } }); const initStats = () => os.api('stats', { }).then((res) => { - stats = res; + stats.value = res; }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'overview', title: i18n.ts.overview, }, { diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index b894c9bbe9..8479b0ddea 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkObjectView from '@/components/MkObjectView.vue'; @@ -82,19 +82,19 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { iAmAdmin, iAmModerator } from '@/account.js'; -let tab = $ref('overview'); -let file: any = $ref(null); -let info: any = $ref(null); -let isSensitive: boolean = $ref(false); +const tab = ref('overview'); +const file = ref<any>(null); +const info = ref<any>(null); +const isSensitive = ref<boolean>(false); const props = defineProps<{ fileId: string, }>(); async function fetch() { - file = await os.api('drive/files/show', { fileId: props.fileId }); - info = await os.api('admin/drive/show-file', { fileId: props.fileId }); - isSensitive = file.isSensitive; + file.value = await os.api('drive/files/show', { fileId: props.fileId }); + info.value = await os.api('admin/drive/show-file', { fileId: props.fileId }); + isSensitive.value = file.value.isSensitive; } fetch(); @@ -102,29 +102,29 @@ fetch(); async function del() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: file.name }), + text: i18n.t('removeAreYouSure', { x: file.value.name }), }); if (canceled) return; os.apiWithDialog('drive/files/delete', { - fileId: file.id, + fileId: file.value.id, }); } async function toggleIsSensitive(v) { await os.api('drive/files/update', { fileId: props.fileId, isSensitive: v }); - isSensitive = v; + isSensitive.value = v; } -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ text: i18n.ts.openInNewTab, icon: 'ph-arrow-square-out ph-bold ph-lg', handler: () => { - window.open(file.url, '_blank'); + window.open(file.value.url, '_blank', 'noopener'); }, }]); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'overview', title: i18n.ts.overview, icon: 'ph-info ph-bold ph-lg', @@ -139,7 +139,7 @@ const headerTabs = $computed(() => [{ }]); definePageMetadata(computed(() => ({ - title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file, + title: file.value ? i18n.ts.file + ': ' + file.value.name : i18n.ts.file, icon: 'ph-file ph-bold ph-lg', }))); </script> diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 25547117a4..5225b4a831 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -109,6 +109,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </MkFolder> + <div> + <MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton> + <MkButton v-if="iAmModerator" inline danger @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton> + </div> <MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton> </div> </FormSection> @@ -117,7 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="tab === 'roles'" class="_gaps"> <MkButton v-if="user.host == null" primary rounded @click="assignRole"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.assign }}</MkButton> - <div v-for="role in info.roles" :key="role.id" :class="$style.roleItem"> + <div v-for="role in info.roles" :key="role.id"> <div :class="$style.roleItemMain"> <MkRolePreview :class="$style.role" :role="role" :forModeration="true"/> <button class="_button" :class="$style.roleToggle" @click="toggleRoleItem(role)"><i class="ph-caret-down ph-bold ph-lg"></i></button> @@ -186,7 +190,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, watch } from 'vue'; +import { computed, defineAsyncComponent, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkChart from '@/components/MkChart.vue'; import MkObjectView from '@/components/MkObjectView.vue'; @@ -203,12 +207,12 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue'; import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; import { url } from '@/config.js'; -import { userPage, acct } from '@/filters/user.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 MkRolePreview from '@/components/MkRolePreview.vue'; -import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import MkPagination from '@/components/MkPagination.vue'; const props = withDefaults(defineProps<{ userId: string; @@ -217,20 +221,19 @@ const props = withDefaults(defineProps<{ initialTab: 'overview', }); -let tab = $ref(props.initialTab); -let chartSrc = $ref('per-user-notes'); -let user = $ref<null | Misskey.entities.UserDetailed>(); -let init = $ref<ReturnType<typeof createFetcher>>(); -let info = $ref(); -let ips = $ref(null); -let ap = $ref(null); -let moderator = $ref(false); -let silenced = $ref(false); -let approved = $ref(false); -let suspended = $ref(false); -let markedAsNSFW = $ref(false); -let moderationNote = $ref(''); - +const tab = ref(props.initialTab); +const chartSrc = ref('per-user-notes'); +const user = ref<null | Misskey.entities.UserDetailed>(); +const init = ref<ReturnType<typeof createFetcher>>(); +const info = ref(); +const ips = ref(null); +const ap = ref(null); +const moderator = ref(false); +const silenced = ref(false); +const approved = ref(false); +const suspended = ref(false); +const markedAsNSFW = ref(false); +const moderationNote = ref(''); const filesPagination = { endpoint: 'admin/drive/files' as const, limit: 10, @@ -245,7 +248,7 @@ const announcementsPagination = { userId: props.userId, })), }; -let expandedRoles = $ref([]); +const expandedRoles = ref([]); function createFetcher() { return () => Promise.all([os.api('users/show', { @@ -255,29 +258,28 @@ function createFetcher() { }), iAmAdmin ? os.api('admin/get-user-ips', { userId: props.userId, }) : Promise.resolve(null)]).then(([_user, _info, _ips]) => { - user = _user; - info = _info; - ips = _ips; - moderator = info.isModerator; - silenced = info.isSilenced; - approved = info.approved; - suspended = info.isSuspended; - moderationNote = info.moderationNote; - markedAsNSFW = info.alwaysMarkNsfw; + user.value = _user; + info.value = _info; + ips.value = _ips; + moderator.value = info.value.isModerator; + silenced.value = info.value.isSilenced; + approved.value = info.value.approved; + suspended.value = info.value.isSuspended; + moderationNote.value = info.value.moderationNote; - watch($$(moderationNote), async () => { - await os.api('admin/update-user-note', { userId: user.id, text: moderationNote }); + watch(moderationNote, async () => { + await os.api('admin/update-user-note', { userId: user.value.id, text: moderationNote.value }); await refreshUser(); }); }); } function refreshUser() { - init = createFetcher(); + init.value = createFetcher(); } async function updateRemoteUser() { - await os.apiWithDialog('federation/update-remote-user', { userId: user.id }); + await os.apiWithDialog('federation/update-remote-user', { userId: user.value.id }); refreshUser(); } @@ -290,7 +292,7 @@ async function resetPassword() { return; } else { const { password } = await os.api('admin/reset-password', { - userId: user.id, + userId: user.value.id, }); os.alert({ type: 'success', @@ -305,9 +307,9 @@ async function toggleNSFW(v) { text: v ? i18n.ts.nsfwConfirm : i18n.ts.unNsfwConfirm, }); if (confirm.canceled) { - markedAsNSFW = !v; + markedAsNSFW.value = !v; } else { - await os.api(v ? 'admin/nsfw-user' : 'admin/unnsfw-user', { userId: user.id }); + await os.api(v ? 'admin/nsfw-user' : 'admin/unnsfw-user', { userId: user.value.id }); await refreshUser(); } } @@ -318,9 +320,9 @@ async function toggleSilence(v) { text: v ? i18n.ts.silenceConfirm : i18n.ts.unsilenceConfirm, }); if (confirm.canceled) { - silenced = !v; + silenced.value = !v; } else { - await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: user.id }); + await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: user.value.id }); await refreshUser(); } } @@ -331,13 +333,51 @@ async function toggleSuspend(v) { text: v ? i18n.ts.suspendConfirm : i18n.ts.unsuspendConfirm, }); if (confirm.canceled) { - suspended = !v; + suspended.value = !v; } else { - await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: user.id }); + await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: user.value.id }); await refreshUser(); } } +async function unsetUserAvatar() { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.unsetUserAvatarConfirm, + }); + if (confirm.canceled) return; + const process = async () => { + await os.api('admin/unset-user-avatar', { userId: user.value.id }); + os.success(); + }; + await process().catch(err => { + os.alert({ + type: 'error', + text: err.toString(), + }); + }); + refreshUser(); +} + +async function unsetUserBanner() { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.unsetUserBannerConfirm, + }); + if (confirm.canceled) return; + const process = async () => { + await os.api('admin/unset-user-banner', { userId: user.value.id }); + os.success(); + }; + await process().catch(err => { + os.alert({ + type: 'error', + text: err.toString(), + }); + }); + refreshUser(); +} + async function deleteAllFiles() { const confirm = await os.confirm({ type: 'warning', @@ -345,7 +385,7 @@ async function deleteAllFiles() { }); if (confirm.canceled) return; const process = async () => { - await os.api('admin/delete-all-files-of-a-user', { userId: user.id }); + await os.api('admin/delete-all-files-of-a-user', { userId: user.value.id }); os.success(); }; await process().catch(err => { @@ -365,13 +405,13 @@ async function deleteAccount() { if (confirm.canceled) return; const typed = await os.inputText({ - text: i18n.t('typeToConfirm', { x: user?.username }), + text: i18n.t('typeToConfirm', { x: user.value?.username }), }); if (typed.canceled) return; - if (typed.result === user?.username) { + if (typed.result === user.value?.username) { await os.apiWithDialog('admin/delete-account', { - userId: user.id, + userId: user.value.id, }); } else { os.alert({ @@ -414,7 +454,7 @@ async function assignRole() { : period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30) : null; - await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id, expiresAt }); + await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.value.id, expiresAt }); refreshUser(); } @@ -424,50 +464,50 @@ async function unassignRole(role, ev) { icon: 'ph-x ph-bold ph-lg', danger: true, action: async () => { - await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.id }); + await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.value.id }); refreshUser(); }, }], ev.currentTarget ?? ev.target); } function toggleRoleItem(role) { - if (expandedRoles.includes(role.id)) { - expandedRoles = expandedRoles.filter(x => x !== role.id); + if (expandedRoles.value.includes(role.id)) { + expandedRoles.value = expandedRoles.value.filter(x => x !== role.id); } else { - expandedRoles.push(role.id); + expandedRoles.value.push(role.id); } } function createAnnouncement() { os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), { - user, + user: user.value, }, {}, 'closed'); } function editAnnouncement(announcement) { os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), { - user, + user: user.value, announcement, }, {}, 'closed'); } watch(() => props.userId, () => { - init = createFetcher(); + init.value = createFetcher(); }, { immediate: true, }); -watch($$(user), () => { +watch(user, () => { os.api('ap/get', { - uri: user.uri ?? `${url}/users/${user.id}`, + uri: user.value.uri ?? `${url}/users/${user.value.id}`, }).then(res => { - ap = res; + ap.value = res; }); }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'overview', title: i18n.ts.overview, icon: 'ph-info ph-bold ph-lg', @@ -494,7 +534,7 @@ const headerTabs = $computed(() => [{ }]); definePageMetadata(computed(() => ({ - title: user ? acct(user) : i18n.ts.userInfo, + title: user.value ? acct(user.value) : i18n.ts.userInfo, icon: 'ph-warning-circle ph-bold ph-lg', }))); </script> @@ -609,9 +649,6 @@ definePageMetadata(computed(() => ({ } } -.roleItem { -} - .roleItemMain { display: flex; } diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue index 43bfbeb870..353030b1b9 100644 --- a/packages/frontend/src/pages/admin/_header_.vue +++ b/packages/frontend/src/pages/admin/_header_.vue @@ -69,7 +69,7 @@ const metadata = injectPageMetadata(); const el = shallowRef<HTMLElement>(null); const tabRefs = {}; -const tabHighlightEl = $shallowRef<HTMLElement | null>(null); +const tabHighlightEl = shallowRef<HTMLElement | null>(null); const bg = ref(null); const height = ref(0); const hasTabs = computed(() => { @@ -131,13 +131,13 @@ onMounted(() => { watch(() => [props.tab, props.tabs], () => { nextTick(() => { const tabEl = tabRefs[props.tab]; - if (tabEl && tabHighlightEl) { + if (tabEl && tabHighlightEl.value) { // offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある // https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4 const parentRect = tabEl.parentElement.getBoundingClientRect(); const rect = tabEl.getBoundingClientRect(); - tabHighlightEl.style.width = rect.width + 'px'; - tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px'; + tabHighlightEl.value.style.width = rect.width + 'px'; + tabHighlightEl.value.style.left = (rect.left - parentRect.left) + 'px'; } }); }, { diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue index d670cc7913..92688989d2 100644 --- a/packages/frontend/src/pages/admin/abuses.vue +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, shallowRef, ref } from 'vue'; import XHeader from './_header_.vue'; import MkSelect from '@/components/MkSelect.vue'; @@ -61,31 +61,31 @@ import XAbuseReport from '@/components/MkAbuseReport.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let reports = $shallowRef<InstanceType<typeof MkPagination>>(); +const reports = shallowRef<InstanceType<typeof MkPagination>>(); -let state = $ref('unresolved'); -let reporterOrigin = $ref('combined'); -let targetUserOrigin = $ref('combined'); -let searchUsername = $ref(''); -let searchHost = $ref(''); +const state = ref('unresolved'); +const reporterOrigin = ref('combined'); +const targetUserOrigin = ref('combined'); +const searchUsername = ref(''); +const searchHost = ref(''); const pagination = { endpoint: 'admin/abuse-user-reports' as const, limit: 10, params: computed(() => ({ - state, - reporterOrigin, - targetUserOrigin, + state: state.value, + reporterOrigin: reporterOrigin.value, + targetUserOrigin: targetUserOrigin.value, })), }; function resolved(reportId) { - reports.removeItem(reportId); + reports.value.removeItem(reportId); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.abuseReports, diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index c45d0f8b04..9de9da7d98 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -9,12 +9,15 @@ SPDX-License-Identifier: AGPL-3.0-only <XHeader :actions="headerActions" :tabs="headerTabs"/> </template> <MkSpacer :contentMax="900"> - <MkSwitch :modelValue="publishing" @update:modelValue="onChangePublishing"> - {{ i18n.ts.publishing }} - </MkSwitch> + <MkSelect v-model="filterType" :class="$style.input" @update:modelValue="filterItems"> + <template #label>{{ i18n.ts.state }}</template> + <option value="all">{{ i18n.ts.all }}</option> + <option value="publishing">{{ i18n.ts.publishing }}</option> + <option value="expired">{{ i18n.ts.expired }}</option> + </MkSelect> <div> <div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad"> - <MkAd v-if="ad.url" :specify="ad"/> + <MkAd v-if="ad.url" :key="ad.id" :specify="ad"/> <MkInput v-model="ad.url" type="url"> <template #label>URL</template> </MkInput> @@ -82,43 +85,53 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkFolder from '@/components/MkFolder.vue'; -import MkSwitch from '@/components/MkSwitch.vue'; +import MkSelect from '@/components/MkSelect.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let ads: any[] = $ref([]); +const ads = ref<any[]>([]); // ISO形式はTZがUTCになってしまうので、TZ分ずらして時間を初期化 const localTime = new Date(); const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000; const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday]; -let publishing = false; +const filterType = ref('all'); +let publishing: boolean | null = null; os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => { - ads = adsResponse.map(r => { - const exdate = new Date(r.expiresAt); - const stdate = new Date(r.startsAt); - exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff); - stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff); - return { - ...r, - expiresAt: exdate.toISOString().slice(0, 16), - startsAt: stdate.toISOString().slice(0, 16), - }; - }); + if (adsResponse != null) { + ads.value = adsResponse.map(r => { + const exdate = new Date(r.expiresAt); + const stdate = new Date(r.startsAt); + exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff); + stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff); + return { + ...r, + expiresAt: exdate.toISOString().slice(0, 16), + startsAt: stdate.toISOString().slice(0, 16), + }; + }); + } }); -const onChangePublishing = (v) => { - publishing = v; +const filterItems = (v) => { + if (v === 'publishing') { + publishing = true; + } else if (v === 'expired') { + publishing = false; + } else { + publishing = null; + } + refresh(); }; @@ -128,7 +141,7 @@ function toggleDayOfWeek(ad, index) { } function add() { - ads.unshift({ + ads.value.unshift({ id: null, memo: '', place: 'square', @@ -148,7 +161,7 @@ function remove(ad) { text: i18n.t('removeAreYouSure', { x: ad.url }), }).then(({ canceled }) => { if (canceled) return; - ads = ads.filter(x => x !== ad); + ads.value = ads.value.filter(x => x !== ad); if (ad.id == null) return; os.apiWithDialog('admin/ad/delete', { id: ad.id, @@ -196,8 +209,9 @@ function save(ad) { } function more() { - os.api('admin/ad/list', { untilId: ads.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => { - ads = ads.concat(adsResponse.map(r => { + os.api('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); const stdate = new Date(r.startsAt); exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff); @@ -213,7 +227,8 @@ function more() { function refresh() { os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => { - ads = adsResponse.map(r => { + if (adsResponse == null) return; + ads.value = adsResponse.map(r => { const exdate = new Date(r.expiresAt); const stdate = new Date(r.startsAt); exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff); @@ -229,14 +244,14 @@ function refresh() { refresh(); -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ asFullButton: true, icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.add, handler: add, }]); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.ads, @@ -252,4 +267,7 @@ definePageMetadata({ margin-bottom: var(--margin); } } +.input { + margin-bottom: 32px; +} </style> diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index 77649c6c4a..931bd9bbc8 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-model="announcement.title"> <template #label>{{ i18n.ts.title }}</template> </MkInput> - <MkTextarea v-model="announcement.text"> + <MkTextarea v-model="announcement.text" mfmAutocomplete :mfmPreview="true"> <template #label>{{ i18n.ts.text }}</template> </MkTextarea> <MkInput v-model="announcement.imageUrl" type="url"> @@ -71,11 +71,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; 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 MkRadios from '@/components/MkRadios.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -83,15 +82,16 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkFolder from '@/components/MkFolder.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; -let announcements: any[] = $ref([]); +const announcements = ref<any[]>([]); os.api('admin/announcements/list').then(announcementResponse => { - announcements = announcementResponse; + announcements.value = announcementResponse; }); function add() { - announcements.unshift({ + announcements.value.unshift({ _id: Math.random().toString(36), id: null, title: 'New announcement', @@ -111,7 +111,7 @@ function del(announcement) { text: i18n.t('deleteAreYouSure', { x: announcement.title }), }).then(({ canceled }) => { if (canceled) return; - announcements = announcements.filter(x => x !== announcement); + announcements.value = announcements.value.filter(x => x !== announcement); os.api('admin/announcements/delete', announcement); }); } @@ -134,27 +134,27 @@ async function save(announcement) { } function more() { - os.api('admin/announcements/list', { untilId: announcements.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id }).then(announcementResponse => { - announcements = announcements.concat(announcementResponse); + os.api('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 => { - announcements = announcementResponse; + announcements.value = announcementResponse; }); } refresh(); -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ asFullButton: true, icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.add, handler: add, }]); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.announcements, diff --git a/packages/frontend/src/pages/admin/approvals.vue b/packages/frontend/src/pages/admin/approvals.vue index 5d9c9de03b..7d0535bd7f 100644 --- a/packages/frontend/src/pages/admin/approvals.vue +++ b/packages/frontend/src/pages/admin/approvals.vue @@ -44,9 +44,9 @@ function deleted(id: string) { } } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(computed(() => ({ title: i18n.ts.approvals, diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index 450c0ec663..034a6fdcc5 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -64,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; +import { defineAsyncComponent, ref } from 'vue'; import MkRadios from '@/components/MkRadios.vue'; import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; @@ -76,37 +76,37 @@ import { i18n } from '@/i18n.js'; const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue')); -let provider = $ref(null); -let hcaptchaSiteKey: string | null = $ref(null); -let hcaptchaSecretKey: string | null = $ref(null); -let recaptchaSiteKey: string | null = $ref(null); -let recaptchaSecretKey: string | null = $ref(null); -let turnstileSiteKey: string | null = $ref(null); -let turnstileSecretKey: string | null = $ref(null); +const provider = ref(null); +const hcaptchaSiteKey = ref<string | null>(null); +const hcaptchaSecretKey = 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'); - hcaptchaSiteKey = meta.hcaptchaSiteKey; - hcaptchaSecretKey = meta.hcaptchaSecretKey; - recaptchaSiteKey = meta.recaptchaSiteKey; - recaptchaSecretKey = meta.recaptchaSecretKey; - turnstileSiteKey = meta.turnstileSiteKey; - turnstileSecretKey = meta.turnstileSecretKey; + hcaptchaSiteKey.value = meta.hcaptchaSiteKey; + hcaptchaSecretKey.value = meta.hcaptchaSecretKey; + recaptchaSiteKey.value = meta.recaptchaSiteKey; + recaptchaSecretKey.value = meta.recaptchaSecretKey; + turnstileSiteKey.value = meta.turnstileSiteKey; + turnstileSecretKey.value = meta.turnstileSecretKey; - provider = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : meta.enableTurnstile ? 'turnstile' : null; + provider.value = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : meta.enableTurnstile ? 'turnstile' : null; } function save() { os.apiWithDialog('admin/update-meta', { - enableHcaptcha: provider === 'hcaptcha', - hcaptchaSiteKey, - hcaptchaSecretKey, - enableRecaptcha: provider === 'recaptcha', - recaptchaSiteKey, - recaptchaSecretKey, - enableTurnstile: provider === 'turnstile', - turnstileSiteKey, - turnstileSecretKey, + enableHcaptcha: provider.value === 'hcaptcha', + hcaptchaSiteKey: hcaptchaSiteKey.value, + hcaptchaSecretKey: hcaptchaSecretKey.value, + enableRecaptcha: provider.value === 'recaptcha', + recaptchaSiteKey: recaptchaSiteKey.value, + recaptchaSecretKey: recaptchaSecretKey.value, + enableTurnstile: provider.value === 'turnstile', + turnstileSiteKey: turnstileSiteKey.value, + turnstileSecretKey: turnstileSecretKey.value, }).then(() => { fetchInstance(); }); diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index 96a2d8a300..3750a84aac 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -101,14 +101,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import JSON5 from 'json5'; import XHeader from './_header_.vue'; -import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; -import FormSection from '@/components/form/section.vue'; -import FormSplit from '@/components/form/split.vue'; import FromSlot from '@/components/form/slot.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; @@ -119,51 +116,51 @@ import MkButton from '@/components/MkButton.vue'; import MkColorInput from '@/components/MkColorInput.vue'; import { host } from '@/config.js'; -let iconUrl: string | null = $ref(null); -let app192IconUrl: string | null = $ref(null); -let app512IconUrl: string | null = $ref(null); -let bannerUrl: string | null = $ref(null); -let backgroundImageUrl: string | null = $ref(null); -let themeColor: any = $ref(null); -let defaultLightTheme: any = $ref(null); -let defaultDarkTheme: any = $ref(null); -let defaultLike: any = $ref(null); -let serverErrorImageUrl: string | null = $ref(null); -let infoImageUrl: string | null = $ref(null); -let notFoundImageUrl: string | null = $ref(null); -let manifestJsonOverride: string = $ref('{}'); +const iconUrl = ref<string | null>(null); +const app192IconUrl = ref<string | null>(null); +const app512IconUrl = ref<string | null>(null); +const bannerUrl = ref<string | null>(null); +const backgroundImageUrl = ref<string | null>(null); +const themeColor = ref<any>(null); +const defaultLightTheme = ref<any>(null); +const defaultDarkTheme = ref<any>(null); +const defaultLike = ref<string>(''); +const serverErrorImageUrl = ref<string | null>(null); +const infoImageUrl = ref<string | null>(null); +const notFoundImageUrl = ref<string | null>(null); +const manifestJsonOverride = ref<string>('{}'); async function init() { const meta = await os.api('admin/meta'); - iconUrl = meta.iconUrl; - app192IconUrl = meta.app192IconUrl; - app512IconUrl = meta.app512IconUrl; - bannerUrl = meta.bannerUrl; - backgroundImageUrl = meta.backgroundImageUrl; - themeColor = meta.themeColor; - defaultLightTheme = meta.defaultLightTheme; - defaultDarkTheme = meta.defaultDarkTheme; - defaultLike = meta.defaultLike; - serverErrorImageUrl = meta.serverErrorImageUrl; - infoImageUrl = meta.infoImageUrl; - notFoundImageUrl = meta.notFoundImageUrl; - manifestJsonOverride = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t'); + iconUrl.value = meta.iconUrl; + app192IconUrl.value = meta.app192IconUrl; + app512IconUrl.value = meta.app512IconUrl; + bannerUrl.value = meta.bannerUrl; + backgroundImageUrl.value = meta.backgroundImageUrl; + themeColor.value = meta.themeColor; + defaultLightTheme.value = meta.defaultLightTheme; + defaultDarkTheme.value = meta.defaultDarkTheme; + defaultLike.value = meta.defaultLike; + serverErrorImageUrl.value = meta.serverErrorImageUrl; + infoImageUrl.value = meta.infoImageUrl; + notFoundImageUrl.value = meta.notFoundImageUrl; + manifestJsonOverride.value = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t'); } function save() { os.apiWithDialog('admin/update-meta', { - iconUrl, - app192IconUrl, - app512IconUrl, - bannerUrl, - backgroundImageUrl, - themeColor: themeColor === '' ? null : themeColor, - defaultLightTheme: defaultLightTheme === '' ? null : defaultLightTheme, - defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme, - infoImageUrl, - notFoundImageUrl, - serverErrorImageUrl, - manifestJsonOverride: manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride)), + iconUrl: iconUrl.value, + app192IconUrl: app192IconUrl.value, + app512IconUrl: app512IconUrl.value, + bannerUrl: bannerUrl.value, + backgroundImageUrl: backgroundImageUrl.value, + themeColor: themeColor.value === '' ? null : themeColor.value, + defaultLightTheme: defaultLightTheme.value === '' ? null : defaultLightTheme.value, + defaultDarkTheme: defaultDarkTheme.value === '' ? null : defaultDarkTheme.value, + infoImageUrl: infoImageUrl.value, + notFoundImageUrl: notFoundImageUrl.value, + serverErrorImageUrl: serverErrorImageUrl.value, + manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)), }).then(() => { fetchInstance(); }); @@ -177,12 +174,12 @@ function chooseNewLike(ev: MouseEvent) { defaultLike: emoji, }).then(() => { fetchInstance(); - defaultLike = emoji; + defaultLike.value = emoji as string; }); }); } -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.branding, diff --git a/packages/frontend/src/pages/admin/database.vue b/packages/frontend/src/pages/admin/database.vue index 0c14fc9b5f..d9fc672fbf 100644 --- a/packages/frontend/src/pages/admin/database.vue +++ b/packages/frontend/src/pages/admin/database.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { computed } from 'vue'; import FormSuspense from '@/components/form/suspense.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import * as os from '@/os.js'; @@ -29,9 +29,9 @@ 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 headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.database, diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue index 8dcb5f4726..3fee3d553a 100644 --- a/packages/frontend/src/pages/admin/email-settings.vue +++ b/packages/frontend/src/pages/admin/email-settings.vue @@ -64,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; @@ -78,23 +78,23 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; -let enableEmail: boolean = $ref(false); -let email: any = $ref(null); -let smtpSecure: boolean = $ref(false); -let smtpHost: string = $ref(''); -let smtpPort: number = $ref(0); -let smtpUser: string = $ref(''); -let smtpPass: string = $ref(''); +const enableEmail = ref<boolean>(false); +const email = ref<any>(null); +const smtpSecure = ref<boolean>(false); +const smtpHost = ref<string>(''); +const smtpPort = ref<number>(0); +const smtpUser = ref<string>(''); +const smtpPass = ref<string>(''); async function init() { const meta = await os.api('admin/meta'); - enableEmail = meta.enableEmail; - email = meta.email; - smtpSecure = meta.smtpSecure; - smtpHost = meta.smtpHost; - smtpPort = meta.smtpPort; - smtpUser = meta.smtpUser; - smtpPass = meta.smtpPass; + enableEmail.value = meta.enableEmail; + email.value = meta.email; + smtpSecure.value = meta.smtpSecure; + smtpHost.value = meta.smtpHost; + smtpPort.value = meta.smtpPort; + smtpUser.value = meta.smtpUser; + smtpPass.value = meta.smtpPass; } async function testEmail() { @@ -115,19 +115,19 @@ async function testEmail() { function save() { os.apiWithDialog('admin/update-meta', { - enableEmail, - email, - smtpSecure, - smtpHost, - smtpPort, - smtpUser, - smtpPass, + enableEmail: enableEmail.value, + email: email.value, + smtpSecure: smtpSecure.value, + smtpHost: smtpHost.value, + smtpPort: smtpPort.value, + smtpUser: smtpUser.value, + smtpPass: smtpPass.value, }).then(() => { fetchInstance(); }); } -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.emailServer, diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue index d1967c4bff..f4359270b6 100644 --- a/packages/frontend/src/pages/admin/external-services.vue +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -34,10 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; +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'; @@ -45,27 +46,27 @@ import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let deeplAuthKey: string = $ref(''); -let deeplIsPro: boolean = $ref(false); +const deeplAuthKey = ref<string>(''); +const deeplIsPro = ref<boolean>(false); async function init() { const meta = await os.api('admin/meta'); - deeplAuthKey = meta.deeplAuthKey; - deeplIsPro = meta.deeplIsPro; + deeplAuthKey.value = meta.deeplAuthKey; + deeplIsPro.value = meta.deeplIsPro; } function save() { os.apiWithDialog('admin/update-meta', { - deeplAuthKey, - deeplIsPro, + deeplAuthKey: deeplAuthKey.value, + deeplIsPro: deeplIsPro.value, }).then(() => { fetchInstance(); }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.externalServices, diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue index e09d5181c5..1888a0eb16 100644 --- a/packages/frontend/src/pages/admin/federation.vue +++ b/packages/frontend/src/pages/admin/federation.vue @@ -59,7 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import XHeader from './_header_.vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; @@ -69,25 +69,25 @@ import FormSplit from '@/components/form/split.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let host = $ref(''); -let state = $ref('federating'); -let sort = $ref('+pubSub'); +const host = ref(''); +const state = ref('federating'); +const sort = ref('+pubSub'); const pagination = { endpoint: 'federation/instances' as const, limit: 10, offsetMode: true, params: computed(() => ({ - sort: sort, - host: host !== '' ? host : null, + sort: sort.value, + host: host.value !== '' ? host.value : null, ...( - state === 'federating' ? { federating: true } : - state === 'subscribing' ? { subscribing: true } : - state === 'publishing' ? { publishing: true } : - state === 'suspended' ? { suspended: true } : - state === 'blocked' ? { blocked: true } : - state === 'silenced' ? { silenced: true } : - state === 'notResponding' ? { notResponding: true } : - state === 'nsfw' ? { nsfw: true } : + state.value === 'federating' ? { federating: true } : + state.value === 'subscribing' ? { subscribing: true } : + state.value === 'publishing' ? { publishing: true } : + state.value === 'suspended' ? { suspended: true } : + state.value === 'blocked' ? { blocked: true } : + state.value === 'silenced' ? { silenced: true } : + state.value === 'notResponding' ? { notResponding: true } : + state.value === 'nsfw' ? { nsfw: true } : {}), })), }; @@ -101,9 +101,9 @@ function getStatus(instance) { return 'Alive'; } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(computed(() => ({ title: i18n.ts.federation, diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue index 8935cd96b8..9a355865a4 100644 --- a/packages/frontend/src/pages/admin/files.vue +++ b/packages/frontend/src/pages/admin/files.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import XHeader from './_header_.vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; @@ -45,19 +45,19 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let origin = $ref('local'); -let type = $ref(null); -let searchHost = $ref(''); -let userId = $ref(''); -let viewMode = $ref('grid'); +const origin = ref('local'); +const type = ref(null); +const searchHost = ref(''); +const userId = ref(''); +const viewMode = ref('grid'); const pagination = { endpoint: 'admin/drive/files' as const, limit: 10, params: computed(() => ({ - type: (type && type !== '') ? type : null, - userId: (userId && userId !== '') ? userId : null, - origin: origin, - hostname: (searchHost && searchHost !== '') ? searchHost : null, + type: (type.value && type.value !== '') ? type.value : null, + userId: (userId.value && userId.value !== '') ? userId.value : null, + origin: origin.value, + hostname: (searchHost.value && searchHost.value !== '') ? searchHost.value : null, })), }; @@ -95,7 +95,7 @@ async function find() { }); } -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ text: i18n.ts.lookup, icon: 'ph-magnifying-glass ph-bold ph-lg', handler: find, @@ -105,7 +105,7 @@ const headerActions = $computed(() => [{ handler: clear, }]); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(computed(() => ({ title: i18n.ts.files, diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index cf62767f05..3aa74b1b91 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onActivated, onMounted, onUnmounted, provide, watch } from 'vue'; +import { onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue'; import { i18n } from '@/i18n.js'; import MkSuperMenu from '@/components/MkSuperMenu.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -51,24 +51,24 @@ const indexInfo = { provide('shouldOmitHeaderTitle', false); -let INFO = $ref(indexInfo); -let childInfo = $ref(null); -let narrow = $ref(false); -let view = $ref(null); -let el = $ref(null); -let pageProps = $ref({}); +const INFO = ref(indexInfo); +const childInfo = ref(null); +const narrow = ref(false); +const view = ref(null); +const el = ref(null); +const pageProps = ref({}); let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail); let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile; let noEmailServer = !instance.enableEmail; -let thereIsUnresolvedAbuseReport = $ref(false); -let pendingUserApprovals = $ref(false); -let currentPage = $computed(() => router.currentRef.value.child); +const thereIsUnresolvedAbuseReport = ref(false); +const pendingUserApprovals = ref(false); +const currentPage = computed(() => router.currentRef.value.child); os.api('admin/abuse-user-reports', { state: 'unresolved', limit: 1, }).then(reports => { - if (reports.length > 0) thereIsUnresolvedAbuseReport = true; + if (reports.length > 0) thereIsUnresolvedAbuseReport.value = true; }); os.api('admin/show-users', { @@ -76,16 +76,16 @@ os.api('admin/show-users', { origin: 'local', limit: 1, }).then(approvals => { - if (approvals.length > 0) pendingUserApprovals = true; + if (approvals.length > 0) pendingUserApprovals.value = true; }); const NARROW_THRESHOLD = 600; const ro = new ResizeObserver((entries, observer) => { if (entries.length === 0) return; - narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD; + narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD; }); -const menuDef = $computed(() => [{ +const menuDef = computed(() => [{ title: i18n.ts.quickAction, items: [{ type: 'button', @@ -104,72 +104,72 @@ const menuDef = $computed(() => [{ icon: 'ph-gauge ph-bold ph-lg', text: i18n.ts.dashboard, to: '/admin/overview', - active: currentPage?.route.name === 'overview', + active: currentPage.value?.route.name === 'overview', }, { icon: 'ph-users ph-bold ph-lg', text: i18n.ts.users, to: '/admin/users', - active: currentPage?.route.name === 'users', + active: currentPage.value?.route.name === 'users', }, { icon: 'ph-user-plus ph-bold ph-lg', text: i18n.ts.invite, to: '/admin/invites', - active: currentPage?.route.name === 'invites', + active: currentPage.value?.route.name === 'invites', }, { icon: 'ph-chalkboard-teacher ph-bold ph-lg', text: i18n.ts.approvals, to: '/admin/approvals', - active: currentPage?.route.name === 'approvals', + active: currentPage.value?.route.name === 'approvals', }, { icon: 'ph-seal-check ph-bold ph-lg', text: i18n.ts.roles, to: '/admin/roles', - active: currentPage?.route.name === 'roles', + active: currentPage.value?.route.name === 'roles', }, { icon: 'ph-smiley ph-bold ph-lg', text: i18n.ts.customEmojis, to: '/admin/emojis', - active: currentPage?.route.name === 'emojis', + active: currentPage.value?.route.name === 'emojis', }, { icon: 'ph-sparkle ph-bold ph-lg', text: i18n.ts.avatarDecorations, to: '/admin/avatar-decorations', - active: currentPage?.route.name === 'avatarDecorations', + active: currentPage.value?.route.name === 'avatarDecorations', }, { icon: 'ph-globe-hemisphere-west ph-bold ph-lg', text: i18n.ts.federation, to: '/admin/federation', - active: currentPage?.route.name === 'federation', + active: currentPage.value?.route.name === 'federation', }, { icon: 'ph-clock ph-bold ph-lg-play', text: i18n.ts.jobQueue, to: '/admin/queue', - active: currentPage?.route.name === 'queue', + active: currentPage.value?.route.name === 'queue', }, { icon: 'ph-cloud ph-bold ph-lg', text: i18n.ts.files, to: '/admin/files', - active: currentPage?.route.name === 'files', + active: currentPage.value?.route.name === 'files', }, { icon: 'ph-megaphone ph-bold ph-lg', text: i18n.ts.announcements, to: '/admin/announcements', - active: currentPage?.route.name === 'announcements', + active: currentPage.value?.route.name === 'announcements', }, { icon: 'ph-flag ph-bold ph-lg', text: i18n.ts.ads, to: '/admin/ads', - active: currentPage?.route.name === 'ads', + active: currentPage.value?.route.name === 'ads', }, { icon: 'ph-warning-circle ph-bold ph-lg', text: i18n.ts.abuseReports, to: '/admin/abuses', - active: currentPage?.route.name === 'abuses', + active: currentPage.value?.route.name === 'abuses', }, { icon: 'ph-list ph-bold ph-lg-search', text: i18n.ts.moderationLogs, to: '/admin/modlog', - active: currentPage?.route.name === 'modlog', + active: currentPage.value?.route.name === 'modlog', }], }, { title: i18n.ts.settings, @@ -177,57 +177,57 @@ const menuDef = $computed(() => [{ icon: 'ph-gear ph-bold ph-lg', text: i18n.ts.general, to: '/admin/settings', - active: currentPage?.route.name === 'settings', + active: currentPage.value?.route.name === 'settings', }, { icon: 'ph-paint-roller ph-bold ph-lg', text: i18n.ts.branding, to: '/admin/branding', - active: currentPage?.route.name === 'branding', + active: currentPage.value?.route.name === 'branding', }, { icon: 'ph-shield ph-bold ph-lg', text: i18n.ts.moderation, to: '/admin/moderation', - active: currentPage?.route.name === 'moderation', + active: currentPage.value?.route.name === 'moderation', }, { icon: 'ph-envelope ph-bold ph-lg', text: i18n.ts.emailServer, to: '/admin/email-settings', - active: currentPage?.route.name === 'email-settings', + active: currentPage.value?.route.name === 'email-settings', }, { icon: 'ph-cloud ph-bold ph-lg', text: i18n.ts.objectStorage, to: '/admin/object-storage', - active: currentPage?.route.name === 'object-storage', + active: currentPage.value?.route.name === 'object-storage', }, { icon: 'ph-lock ph-bold ph-lg', text: i18n.ts.security, to: '/admin/security', - active: currentPage?.route.name === 'security', + active: currentPage.value?.route.name === 'security', }, { icon: 'ph-planet ph-bold ph-lg', text: i18n.ts.relays, to: '/admin/relays', - active: currentPage?.route.name === 'relays', + active: currentPage.value?.route.name === 'relays', }, { icon: 'ph-prohibit ph-bold ph-lg', text: i18n.ts.instanceBlocking, to: '/admin/instance-block', - active: currentPage?.route.name === 'instance-block', + active: currentPage.value?.route.name === 'instance-block', }, { icon: 'ph-ghost ph-bold ph-lg', text: i18n.ts.proxyAccount, to: '/admin/proxy-account', - active: currentPage?.route.name === 'proxy-account', + active: currentPage.value?.route.name === 'proxy-account', }, { icon: 'ph-arrow-square-out ph-bold ph-lg', text: i18n.ts.externalServices, to: '/admin/external-services', - active: currentPage?.route.name === 'external-services', + active: currentPage.value?.route.name === 'external-services', }, { icon: 'ph-faders ph-bold ph-lg', text: i18n.ts.other, to: '/admin/other-settings', - active: currentPage?.route.name === 'other-settings', + active: currentPage.value?.route.name === 'other-settings', }], }, { title: i18n.ts.info, @@ -235,28 +235,28 @@ const menuDef = $computed(() => [{ icon: 'ph-database ph-bold ph-lg', text: i18n.ts.database, to: '/admin/database', - active: currentPage?.route.name === 'database', + active: currentPage.value?.route.name === 'database', }], }]); -watch(narrow, () => { - if (currentPage?.route.name == null && !narrow) { +watch(narrow.value, () => { + if (currentPage.value?.route.name == null && !narrow.value) { router.push('/admin/overview'); } }); onMounted(() => { - ro.observe(el); + ro.observe(el.value); - narrow = el.offsetWidth < NARROW_THRESHOLD; - if (currentPage?.route.name == null && !narrow) { + narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; + if (currentPage.value?.route.name == null && !narrow.value) { router.push('/admin/overview'); } }); onActivated(() => { - narrow = el.offsetWidth < NARROW_THRESHOLD; - if (currentPage?.route.name == null && !narrow) { + narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; + if (currentPage.value?.route.name == null && !narrow.value) { router.push('/admin/overview'); } }); @@ -266,16 +266,17 @@ onUnmounted(() => { }); watch(router.currentRef, (to) => { - if (to.route.path === '/admin' && to.child?.route.name == null && !narrow) { + if (to.route.path === '/admin' && to.child?.route.name == null && !narrow.value) { router.replace('/admin/overview'); } }); provideMetadataReceiver((info) => { if (info == null) { - childInfo = null; + childInfo.value = null; } else { - childInfo = info; + childInfo.value = info; + INFO.value.needWideArea = info.value.needWideArea ?? undefined; } }); @@ -283,7 +284,7 @@ function invite() { os.api('admin/invite/create').then(x => { os.alert({ type: 'info', - text: x?.[0].code, + text: x[0].code, }); }).catch(err => { os.alert({ @@ -327,11 +328,11 @@ function lookup(ev: MouseEvent) { }], ev.currentTarget ?? ev.target); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(INFO); +definePageMetadata(INFO.value); defineExpose({ header: { diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue index 6024ed6b8f..e54f6dc065 100644 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ b/packages/frontend/src/pages/admin/instance-block.vue @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; import MkButton from '@/components/MkButton.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -32,29 +33,29 @@ import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let blockedHosts: string = $ref(''); -let silencedHosts: string = $ref(''); -let tab = $ref('block'); +const blockedHosts = ref<string>(''); +const silencedHosts = ref<string>(''); +const tab = ref('block'); async function init() { const meta = await os.api('admin/meta'); - blockedHosts = meta.blockedHosts.join('\n'); - silencedHosts = meta.silencedHosts.join('\n'); + blockedHosts.value = meta.blockedHosts.join('\n'); + silencedHosts.value = meta.silencedHosts.join('\n'); } function save() { os.apiWithDialog('admin/update-meta', { - blockedHosts: blockedHosts.split('\n') || [], - silencedHosts: silencedHosts.split('\n') || [], + blockedHosts: blockedHosts.value.split('\n') || [], + silencedHosts: silencedHosts.value.split('\n') || [], }).then(() => { fetchInstance(); }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'block', title: i18n.ts.block, icon: 'ph-prohibit ph-bold ph-lg', diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue index 406317f660..6314d0ce4e 100644 --- a/packages/frontend/src/pages/admin/invites.vue +++ b/packages/frontend/src/pages/admin/invites.vue @@ -70,8 +70,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); -let type = ref('all'); -let sort = ref('+createdAt'); +const type = ref('all'); +const sort = ref('+createdAt'); const pagination: Paging = { endpoint: 'admin/invite/list' as const, @@ -109,8 +109,8 @@ function deleted(id: string) { } } -const headerActions = $computed(() => []); -const headerTabs = $computed(() => []); +const headerActions = computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.invite, diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index cacb3254a8..9539611f76 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -48,6 +48,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.sensitiveWords }}</template> <template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template> </MkTextarea> + + <MkTextarea v-model="hiddenTags"> + <template #label>{{ i18n.ts.hiddenTags }}</template> + <template #caption>{{ i18n.ts.hiddenTagsDescription }}</template> + </MkTextarea> </div> </FormSuspense> </MkSpacer> @@ -63,13 +68,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; -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 { fetchInstance } from '@/instance.js'; @@ -78,45 +81,48 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import FormLink from '@/components/form/link.vue'; -let enableRegistration: boolean = $ref(false); -let emailRequiredForSignup: boolean = $ref(false); -let approvalRequiredForSignup: boolean = $ref(false); -let bubbleTimelineEnabled: boolean = $ref(false); -let sensitiveWords: string = $ref(''); -let preservedUsernames: string = $ref(''); -let bubbleTimeline: string = $ref(''); -let tosUrl: string | null = $ref(null); -let privacyPolicyUrl: string | null = $ref(null); +const enableRegistration = ref<boolean>(false); +const emailRequiredForSignup = ref<boolean>(false); +const approvalRequiredForSignup = ref<boolean>(false); +const bubbleTimelineEnabled = ref<boolean>(false); +const sensitiveWords = ref<string>(''); +const hiddenTags = ref<string>(''); +const preservedUsernames = ref<string>(''); +const bubbleTimeline = ref<string>(''); +const tosUrl = ref<string | null>(null); +const privacyPolicyUrl = ref<string | null>(null); async function init() { const meta = await os.api('admin/meta'); - enableRegistration = !meta.disableRegistration; - emailRequiredForSignup = meta.emailRequiredForSignup; - approvalRequiredForSignup = meta.approvalRequiredForSignup; - sensitiveWords = meta.sensitiveWords.join('\n'); - preservedUsernames = meta.preservedUsernames.join('\n'); - tosUrl = meta.tosUrl; - privacyPolicyUrl = meta.privacyPolicyUrl; - bubbleTimeline = meta.bubbleInstances.join('\n'); - bubbleTimelineEnabled = meta.policies.btlAvailable; + enableRegistration.value = !meta.disableRegistration; + emailRequiredForSignup.value = meta.emailRequiredForSignup; + approvalRequiredForSignup.value = meta.approvalRequiredForSignup; + sensitiveWords.value = meta.sensitiveWords.join('\n'); + hiddenTags.value = meta.hiddenTags.join('\n'); + preservedUsernames.value = meta.preservedUsernames.join('\n'); + tosUrl.value = meta.tosUrl; + privacyPolicyUrl.value = meta.privacyPolicyUrl; + bubbleTimeline.value = meta.bubbleInstances.join('\n'); + bubbleTimelineEnabled.value = meta.policies.btlAvailable; } function save() { os.apiWithDialog('admin/update-meta', { - disableRegistration: !enableRegistration, - emailRequiredForSignup, - approvalRequiredForSignup, - tosUrl, - privacyPolicyUrl, - sensitiveWords: sensitiveWords.split('\n'), - preservedUsernames: preservedUsernames.split('\n'), - bubbleInstances: bubbleTimeline.split('\n'), + disableRegistration: !enableRegistration.value, + emailRequiredForSignup: emailRequiredForSignup.value, + approvalRequiredForSignup: approvalRequiredForSignup.value, + tosUrl: tosUrl.value, + privacyPolicyUrl: privacyPolicyUrl.value, + sensitiveWords: sensitiveWords.value.split('\n'), + hiddenTags: hiddenTags.value.split('\n'), + preservedUsernames: preservedUsernames.value.split('\n'), + bubbleInstances: bubbleTimeline.value.split('\n'), }).then(() => { fetchInstance(); }); } -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.moderation, diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index a6612f17d1..4436fbac89 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTime :time="log.createdAt"/> </template> - <div :class="$style.root"> + <div> <div style="display: flex; gap: var(--margin); flex-wrap: wrap;"> <div style="flex: 1;">{{ i18n.ts.moderator }}: <MkA :to="`/admin/user/${log.userId}`" class="_link">@{{ log.user?.username }}</MkA></div> <div style="flex: 1;">{{ i18n.ts.dateAndTime }}: <MkTime :time="log.createdAt" mode="detail"/></div> @@ -127,9 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only import * as Misskey from 'misskey-js'; import { CodeDiff } from 'v-code-diff'; import JSON5 from 'json5'; -import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { dateString } from '@/filters/date.js'; import MkFolder from '@/components/MkFolder.vue'; const props = defineProps<{ @@ -138,9 +136,6 @@ const props = defineProps<{ </script> <style lang="scss" module> -.root { -} - .avatar { width: 18px; height: 18px; diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue index bdbed67ad1..a50d401ba2 100644 --- a/packages/frontend/src/pages/admin/modlog.vue +++ b/packages/frontend/src/pages/admin/modlog.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, shallowRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XHeader from './_header_.vue'; import XModLog from './modlog.ModLog.vue'; @@ -40,25 +40,25 @@ import MkPagination from '@/components/MkPagination.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let logs = $shallowRef<InstanceType<typeof MkPagination>>(); +const logs = shallowRef<InstanceType<typeof MkPagination>>(); -let type = $ref(null); -let moderatorId = $ref(''); +const type = ref(null); +const moderatorId = ref(''); const pagination = { endpoint: 'admin/show-moderation-logs' as const, limit: 30, params: computed(() => ({ - type, - userId: moderatorId === '' ? null : moderatorId, + type: type.value, + userId: moderatorId.value === '' ? null : moderatorId.value, })), }; console.log(Misskey); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.moderationLogs, diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue index cd1f4ca601..e71e53c942 100644 --- a/packages/frontend/src/pages/admin/object-storage.vue +++ b/packages/frontend/src/pages/admin/object-storage.vue @@ -83,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; @@ -95,58 +95,58 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; -let useObjectStorage: boolean = $ref(false); -let objectStorageBaseUrl: string | null = $ref(null); -let objectStorageBucket: string | null = $ref(null); -let objectStoragePrefix: string | null = $ref(null); -let objectStorageEndpoint: string | null = $ref(null); -let objectStorageRegion: string | null = $ref(null); -let objectStoragePort: number | null = $ref(null); -let objectStorageAccessKey: string | null = $ref(null); -let objectStorageSecretKey: string | null = $ref(null); -let objectStorageUseSSL: boolean = $ref(false); -let objectStorageUseProxy: boolean = $ref(false); -let objectStorageSetPublicRead: boolean = $ref(false); -let objectStorageS3ForcePathStyle: boolean = $ref(true); +const useObjectStorage = ref<boolean>(false); +const objectStorageBaseUrl = ref<string | null>(null); +const objectStorageBucket = ref<string | null>(null); +const objectStoragePrefix = ref<string | null>(null); +const objectStorageEndpoint = ref<string | null>(null); +const objectStorageRegion = ref<string | null>(null); +const objectStoragePort = ref<number | null>(null); +const objectStorageAccessKey = ref<string | null>(null); +const objectStorageSecretKey = ref<string | null>(null); +const objectStorageUseSSL = ref<boolean>(false); +const objectStorageUseProxy = ref<boolean>(false); +const objectStorageSetPublicRead = ref<boolean>(false); +const objectStorageS3ForcePathStyle = ref<boolean>(true); async function init() { const meta = await os.api('admin/meta'); - useObjectStorage = meta.useObjectStorage; - objectStorageBaseUrl = meta.objectStorageBaseUrl; - objectStorageBucket = meta.objectStorageBucket; - objectStoragePrefix = meta.objectStoragePrefix; - objectStorageEndpoint = meta.objectStorageEndpoint; - objectStorageRegion = meta.objectStorageRegion; - objectStoragePort = meta.objectStoragePort; - objectStorageAccessKey = meta.objectStorageAccessKey; - objectStorageSecretKey = meta.objectStorageSecretKey; - objectStorageUseSSL = meta.objectStorageUseSSL; - objectStorageUseProxy = meta.objectStorageUseProxy; - objectStorageSetPublicRead = meta.objectStorageSetPublicRead; - objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle; + useObjectStorage.value = meta.useObjectStorage; + objectStorageBaseUrl.value = meta.objectStorageBaseUrl; + objectStorageBucket.value = meta.objectStorageBucket; + objectStoragePrefix.value = meta.objectStoragePrefix; + objectStorageEndpoint.value = meta.objectStorageEndpoint; + objectStorageRegion.value = meta.objectStorageRegion; + objectStoragePort.value = meta.objectStoragePort; + objectStorageAccessKey.value = meta.objectStorageAccessKey; + objectStorageSecretKey.value = meta.objectStorageSecretKey; + objectStorageUseSSL.value = meta.objectStorageUseSSL; + objectStorageUseProxy.value = meta.objectStorageUseProxy; + objectStorageSetPublicRead.value = meta.objectStorageSetPublicRead; + objectStorageS3ForcePathStyle.value = meta.objectStorageS3ForcePathStyle; } function save() { os.apiWithDialog('admin/update-meta', { - useObjectStorage, - objectStorageBaseUrl, - objectStorageBucket, - objectStoragePrefix, - objectStorageEndpoint, - objectStorageRegion, - objectStoragePort, - objectStorageAccessKey, - objectStorageSecretKey, - objectStorageUseSSL, - objectStorageUseProxy, - objectStorageSetPublicRead, - objectStorageS3ForcePathStyle, + useObjectStorage: useObjectStorage.value, + objectStorageBaseUrl: objectStorageBaseUrl.value, + objectStorageBucket: objectStorageBucket.value, + objectStoragePrefix: objectStoragePrefix.value, + objectStorageEndpoint: objectStorageEndpoint.value, + objectStorageRegion: objectStorageRegion.value, + objectStoragePort: objectStoragePort.value, + objectStorageAccessKey: objectStorageAccessKey.value, + objectStorageSecretKey: objectStorageSecretKey.value, + objectStorageUseSSL: objectStorageUseSSL.value, + objectStorageUseProxy: objectStorageUseProxy.value, + objectStorageSetPublicRead: objectStorageSetPublicRead.value, + objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value, }).then(() => { fetchInstance(); }); } -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.objectStorage, diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue index d1bc420db8..6523676a18 100644 --- a/packages/frontend/src/pages/admin/other-settings.vue +++ b/packages/frontend/src/pages/admin/other-settings.vue @@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; @@ -66,44 +66,44 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkSwitch from '@/components/MkSwitch.vue'; -let enableServerMachineStats: boolean = $ref(false); -let enableAchievements: boolean = $ref(false); -let enableBotTrending: boolean = $ref(false); -let enableIdenticonGeneration: boolean = $ref(false); -let enableChartsForRemoteUser: boolean = $ref(false); -let enableChartsForFederatedInstances: boolean = $ref(false); +const enableServerMachineStats = ref<boolean>(false); +const enableAchievements = ref<boolean>(false); +const enableBotTrending = ref<boolean>(false); +const enableIdenticonGeneration = ref<boolean>(false); +const enableChartsForRemoteUser = ref<boolean>(false); +const enableChartsForFederatedInstances = ref<boolean>(false); async function init() { const meta = await os.api('admin/meta'); - enableServerMachineStats = meta.enableServerMachineStats; - enableAchievements = meta.enableAchievements; - enableBotTrending = meta.enableBotTrending; - enableIdenticonGeneration = meta.enableIdenticonGeneration; - enableChartsForRemoteUser = meta.enableChartsForRemoteUser; - enableChartsForFederatedInstances = meta.enableChartsForFederatedInstances; + enableServerMachineStats.value = meta.enableServerMachineStats; + enableAchievements.value = meta.enableAchievements; + enableBotTrending.value = meta.enableBotTrending; + enableIdenticonGeneration.value = meta.enableIdenticonGeneration; + enableChartsForRemoteUser.value = meta.enableChartsForRemoteUser; + enableChartsForFederatedInstances.value = meta.enableChartsForFederatedInstances; } function save() { os.apiWithDialog('admin/update-meta', { - enableServerMachineStats, - enableAchievements, - enableBotTrending, - enableIdenticonGeneration, - enableChartsForRemoteUser, - enableChartsForFederatedInstances, + enableServerMachineStats: enableServerMachineStats.value, + enableAchievements: enableAchievements.value, + enableBotTrending: enableBotTrending.value, + enableIdenticonGeneration: enableIdenticonGeneration.value, + enableChartsForRemoteUser: enableChartsForRemoteUser.value, + enableChartsForFederatedInstances: enableChartsForFederatedInstances.value, }).then(() => { fetchInstance(); }); } -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ asFullButton: true, icon: 'ph-check ph-bold ph-lg', text: i18n.ts.save, handler: save, }]); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.other, diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue index 8426c463d2..5e67370c2b 100644 --- a/packages/frontend/src/pages/admin/overview.active-users.vue +++ b/packages/frontend/src/pages/admin/overview.active-users.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import gradient from 'chartjs-plugin-gradient'; import * as os from '@/os.js'; @@ -24,11 +24,11 @@ import { initChart } from '@/scripts/init-chart.js'; initChart(); -const chartEl = $shallowRef<HTMLCanvasElement>(null); +const chartEl = shallowRef<HTMLCanvasElement>(null); const now = new Date(); let chartInstance: Chart = null; const chartLimit = 7; -let fetching = $ref(true); +const fetching = ref(true); const { handler: externalTooltipHandler } = useChartTooltip(); @@ -61,7 +61,7 @@ async function renderChart() { const max = Math.max(...raw.read); - chartInstance = new Chart(chartEl, { + chartInstance = new Chart(chartEl.value, { type: 'bar', data: { datasets: [{ @@ -155,7 +155,7 @@ async function renderChart() { plugins: [chartVLine(vLineColor)], }); - fetching = false; + fetching.value = false; } onMounted(async () => { diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue index cd54041c34..0de62fadea 100644 --- a/packages/frontend/src/pages/admin/overview.ap-requests.vue +++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import gradient from 'chartjs-plugin-gradient'; import * as os from '@/os.js'; @@ -33,9 +33,9 @@ import { initChart } from '@/scripts/init-chart.js'; initChart(); const chartLimit = 50; -const chartEl = $shallowRef<HTMLCanvasElement>(); -const chartEl2 = $shallowRef<HTMLCanvasElement>(); -let fetching = $ref(true); +const chartEl = shallowRef<HTMLCanvasElement>(); +const chartEl2 = shallowRef<HTMLCanvasElement>(); +const fetching = ref(true); const { handler: externalTooltipHandler } = useChartTooltip(); const { handler: externalTooltipHandler2 } = useChartTooltip(); @@ -74,7 +74,7 @@ onMounted(async () => { const succMax = Math.max(...raw.deliverSucceeded); const failMax = Math.max(...raw.deliverFailed); - new Chart(chartEl, { + new Chart(chartEl.value, { type: 'line', data: { datasets: [{ @@ -178,7 +178,7 @@ onMounted(async () => { plugins: [chartVLine(vLineColor)], }); - new Chart(chartEl2, { + new Chart(chartEl2.value, { type: 'bar', data: { datasets: [{ @@ -265,7 +265,7 @@ onMounted(async () => { plugins: [chartVLine(vLineColor)], }); - fetching = false; + fetching.value = false; }); </script> diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue index 33dbbd0d84..03e33e57c4 100644 --- a/packages/frontend/src/pages/admin/overview.federation.vue +++ b/packages/frontend/src/pages/admin/overview.federation.vue @@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; import XPie from './overview.pie.vue'; import * as os from '@/os.js'; import number from '@/filters/number.js'; @@ -54,25 +54,25 @@ import MkNumberDiff from '@/components/MkNumberDiff.vue'; import { i18n } from '@/i18n.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; -let topSubInstancesForPie: any = $ref(null); -let topPubInstancesForPie: any = $ref(null); -let federationPubActive = $ref<number | null>(null); -let federationPubActiveDiff = $ref<number | null>(null); -let federationSubActive = $ref<number | null>(null); -let federationSubActiveDiff = $ref<number | null>(null); -let fetching = $ref(true); +const topSubInstancesForPie = ref<any>(null); +const topPubInstancesForPie = ref<any>(null); +const federationPubActive = ref<number | null>(null); +const federationPubActiveDiff = ref<number | null>(null); +const federationSubActive = ref<number | null>(null); +const federationSubActiveDiff = ref<number | null>(null); +const fetching = ref(true); const { handler: externalTooltipHandler } = useChartTooltip(); onMounted(async () => { const chart = await os.apiGet('charts/federation', { limit: 2, span: 'day' }); - federationPubActive = chart.pubActive[0]; - federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1]; - federationSubActive = chart.subActive[0]; - federationSubActiveDiff = chart.subActive[0] - chart.subActive[1]; + 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 => { - topSubInstancesForPie = res.topSubInstances.map(x => ({ + topSubInstancesForPie.value = res.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount, @@ -80,7 +80,7 @@ onMounted(async () => { os.pageWindow(`/instance-info/${x.host}`); }, })).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowersCount }]); - topPubInstancesForPie = res.topPubInstances.map(x => ({ + topPubInstancesForPie.value = res.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount, @@ -90,7 +90,7 @@ onMounted(async () => { })).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowingCount }]); }); - fetching = false; + fetching.value = false; }); </script> diff --git a/packages/frontend/src/pages/admin/overview.heatmap.vue b/packages/frontend/src/pages/admin/overview.heatmap.vue index 4d09f183bf..8e3c809353 100644 --- a/packages/frontend/src/pages/admin/overview.heatmap.vue +++ b/packages/frontend/src/pages/admin/overview.heatmap.vue @@ -17,10 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref } from 'vue'; import MkHeatmap from '@/components/MkHeatmap.vue'; import MkSelect from '@/components/MkSelect.vue'; -let src = $ref('active-users'); +const src = ref('active-users'); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/admin/overview.moderators.vue b/packages/frontend/src/pages/admin/overview.moderators.vue index 4086ca51f0..c6e81b4a18 100644 --- a/packages/frontend/src/pages/admin/overview.moderators.vue +++ b/packages/frontend/src/pages/admin/overview.moderators.vue @@ -17,21 +17,21 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; -let moderators: any = $ref(null); -let fetching = $ref(true); +const moderators = ref<any>(null); +const fetching = ref(true); onMounted(async () => { - moderators = await os.api('admin/show-users', { + moderators.value = await os.api('admin/show-users', { sort: '+lastActiveDate', state: 'adminOrModerator', limit: 30, }); - fetching = false; + fetching.value = false; }); </script> diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue index 1af9d89f62..b6b3bf194a 100644 --- a/packages/frontend/src/pages/admin/overview.queue.vue +++ b/packages/frontend/src/pages/admin/overview.queue.vue @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { markRaw, onMounted, onUnmounted, ref } from 'vue'; +import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue'; import XChart from './overview.queue.chart.vue'; import number from '@/filters/number.js'; import { useStream } from '@/stream.js'; @@ -46,10 +46,10 @@ const activeSincePrevTick = ref(0); const active = ref(0); const delayed = ref(0); const waiting = ref(0); -let chartProcess = $shallowRef<InstanceType<typeof XChart>>(); -let chartActive = $shallowRef<InstanceType<typeof XChart>>(); -let chartDelayed = $shallowRef<InstanceType<typeof XChart>>(); -let chartWaiting = $shallowRef<InstanceType<typeof XChart>>(); +const chartProcess = shallowRef<InstanceType<typeof XChart>>(); +const chartActive = shallowRef<InstanceType<typeof XChart>>(); +const chartDelayed = shallowRef<InstanceType<typeof XChart>>(); +const chartWaiting = shallowRef<InstanceType<typeof XChart>>(); const props = defineProps<{ domain: string; @@ -61,10 +61,10 @@ const onStats = (stats) => { delayed.value = stats[props.domain].delayed; waiting.value = stats[props.domain].waiting; - chartProcess.pushData(stats[props.domain].activeSincePrevTick); - chartActive.pushData(stats[props.domain].active); - chartDelayed.pushData(stats[props.domain].delayed); - chartWaiting.pushData(stats[props.domain].waiting); + chartProcess.value.pushData(stats[props.domain].activeSincePrevTick); + chartActive.value.pushData(stats[props.domain].active); + chartDelayed.value.pushData(stats[props.domain].delayed); + chartWaiting.value.pushData(stats[props.domain].waiting); }; const onStatsLog = (statsLog) => { @@ -80,10 +80,10 @@ const onStatsLog = (statsLog) => { dataWaiting.push(stats[props.domain].waiting); } - chartProcess.setData(dataProcess); - chartActive.setData(dataActive); - chartDelayed.setData(dataDelayed); - chartWaiting.setData(dataWaiting); + chartProcess.value.setData(dataProcess); + chartActive.value.setData(dataActive); + chartDelayed.value.setData(dataDelayed); + chartWaiting.value.setData(dataWaiting); }; onMounted(() => { diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue index 23ed736c94..b853aee55d 100644 --- a/packages/frontend/src/pages/admin/overview.stats.vue +++ b/packages/frontend/src/pages/admin/overview.stats.vue @@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; import * as os from '@/os.js'; import MkNumberDiff from '@/components/MkNumberDiff.vue'; import MkNumber from '@/components/MkNumber.vue'; @@ -69,29 +69,29 @@ import { i18n } from '@/i18n.js'; import { customEmojis } from '@/custom-emojis.js'; import { defaultStore } from '@/store.js'; -let stats: any = $ref(null); -let usersComparedToThePrevDay = $ref<number>(); -let notesComparedToThePrevDay = $ref<number>(); -let onlineUsersCount = $ref(0); -let fetching = $ref(true); +const stats = ref<any>(null); +const usersComparedToThePrevDay = ref<number>(); +const notesComparedToThePrevDay = ref<number>(); +const onlineUsersCount = ref(0); +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), ]); - stats = _stats; - onlineUsersCount = _onlineUsersCount; + stats.value = _stats; + onlineUsersCount.value = _onlineUsersCount; os.apiGet('charts/users', { limit: 2, span: 'day' }).then(chart => { - usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1]; + usersComparedToThePrevDay.value = stats.value.originalUsersCount - chart.local.total[1]; }); os.apiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => { - notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1]; + notesComparedToThePrevDay.value = stats.value.originalNotesCount - chart.local.total[1]; }); - fetching = false; + fetching.value = false; }); </script> diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue index 6ee83c51e7..6b8dd90747 100644 --- a/packages/frontend/src/pages/admin/overview.users.vue +++ b/packages/frontend/src/pages/admin/overview.users.vue @@ -17,13 +17,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref } from 'vue'; import * as os from '@/os.js'; import { useInterval } from '@/scripts/use-interval.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import { defaultStore } from '@/store.js'; -let newUsers = $ref(null); -let fetching = $ref(true); +const newUsers = ref(null); +const fetching = ref(true); const fetch = async () => { const _newUsers = await os.api('admin/show-users', { @@ -31,8 +32,8 @@ const fetch = async () => { sort: '+createdAt', origin: 'local', }); - newUsers = _newUsers; - fetching = false; + newUsers.value = _newUsers; + fetching.value = false; }; useInterval(fetch, 1000 * 60, { diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue index 7563ae9834..edeab30cbb 100644 --- a/packages/frontend/src/pages/admin/overview.vue +++ b/packages/frontend/src/pages/admin/overview.vue @@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue'; +import { markRaw, onMounted, onBeforeUnmount, nextTick, shallowRef, ref, computed } from 'vue'; import XFederation from './overview.federation.vue'; import XInstances from './overview.instances.vue'; import XQueue from './overview.queue.vue'; @@ -82,16 +82,16 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; -const rootEl = $shallowRef<HTMLElement>(); -let serverInfo: any = $ref(null); -let topSubInstancesForPie: any = $ref(null); -let topPubInstancesForPie: any = $ref(null); -let federationPubActive = $ref<number | null>(null); -let federationPubActiveDiff = $ref<number | null>(null); -let federationSubActive = $ref<number | null>(null); -let federationSubActiveDiff = $ref<number | null>(null); -let newUsers = $ref(null); -let activeInstances = $shallowRef(null); +const rootEl = shallowRef<HTMLElement>(); +const serverInfo = ref<any>(null); +const topSubInstancesForPie = ref<any>(null); +const topPubInstancesForPie = ref<any>(null); +const federationPubActive = ref<number | null>(null); +const federationPubActiveDiff = ref<number | null>(null); +const federationSubActive = ref<number | null>(null); +const federationSubActiveDiff = ref<number | null>(null); +const newUsers = ref(null); +const activeInstances = shallowRef(null); const queueStatsConnection = markRaw(useStream().useChannel('queueStats')); const now = new Date(); const filesPagination = { @@ -116,14 +116,14 @@ onMounted(async () => { */ os.apiGet('charts/federation', { limit: 2, span: 'day' }).then(chart => { - federationPubActive = chart.pubActive[0]; - federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1]; - federationSubActive = chart.subActive[0]; - federationSubActiveDiff = chart.subActive[0] - chart.subActive[1]; + 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 => { - topSubInstancesForPie = res.topSubInstances.map(x => ({ + topSubInstancesForPie.value = res.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount, @@ -131,7 +131,7 @@ onMounted(async () => { os.pageWindow(`/instance-info/${x.host}`); }, })).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowersCount }]); - topPubInstancesForPie = res.topPubInstances.map(x => ({ + topPubInstancesForPie.value = res.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount, @@ -142,21 +142,21 @@ onMounted(async () => { }); os.api('admin/server-info').then(serverInfoResponse => { - serverInfo = serverInfoResponse; + serverInfo.value = serverInfoResponse; }); os.api('admin/show-users', { limit: 5, sort: '+createdAt', }).then(res => { - newUsers = res; + newUsers.value = res; }); os.api('federation/instances', { sort: '+latestRequestReceivedAt', limit: 25, }).then(res => { - activeInstances = res; + activeInstances.value = res; }); nextTick(() => { @@ -171,9 +171,9 @@ onBeforeUnmount(() => { queueStatsConnection.dispose(); }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.dashboard, diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue index 3e2a8bdf38..d65e78afa5 100644 --- a/packages/frontend/src/pages/admin/proxy-account.vue +++ b/packages/frontend/src/pages/admin/proxy-account.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -31,36 +31,36 @@ import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let proxyAccount: any = $ref(null); -let proxyAccountId: any = $ref(null); +const proxyAccount = ref<any>(null); +const proxyAccountId = ref<any>(null); async function init() { const meta = await os.api('admin/meta'); - proxyAccountId = meta.proxyAccountId; - if (proxyAccountId) { - proxyAccount = await os.api('users/show', { userId: proxyAccountId }); + proxyAccountId.value = meta.proxyAccountId; + if (proxyAccountId.value) { + proxyAccount.value = await os.api('users/show', { userId: proxyAccountId.value }); } } function chooseProxyAccount() { os.selectUser().then(user => { - proxyAccount = user; - proxyAccountId = user.id; + proxyAccount.value = user; + proxyAccountId.value = user.id; save(); }); } function save() { os.apiWithDialog('admin/update-meta', { - proxyAccountId: proxyAccountId, + proxyAccountId: proxyAccountId.value, }).then(() => { fetchInstance(); }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.proxyAccount, diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue index 4746f9de1b..9612f78624 100644 --- a/packages/frontend/src/pages/admin/queue.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.vue @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { markRaw, onMounted, onUnmounted, ref } from 'vue'; +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'; @@ -63,10 +63,10 @@ const active = ref(0); const delayed = ref(0); const waiting = ref(0); const jobs = ref([]); -let chartProcess = $shallowRef<InstanceType<typeof XChart>>(); -let chartActive = $shallowRef<InstanceType<typeof XChart>>(); -let chartDelayed = $shallowRef<InstanceType<typeof XChart>>(); -let chartWaiting = $shallowRef<InstanceType<typeof XChart>>(); +const chartProcess = shallowRef<InstanceType<typeof XChart>>(); +const chartActive = shallowRef<InstanceType<typeof XChart>>(); +const chartDelayed = shallowRef<InstanceType<typeof XChart>>(); +const chartWaiting = shallowRef<InstanceType<typeof XChart>>(); const props = defineProps<{ domain: string; @@ -78,10 +78,10 @@ const onStats = (stats) => { delayed.value = stats[props.domain].delayed; waiting.value = stats[props.domain].waiting; - chartProcess.pushData(stats[props.domain].activeSincePrevTick); - chartActive.pushData(stats[props.domain].active); - chartDelayed.pushData(stats[props.domain].delayed); - chartWaiting.pushData(stats[props.domain].waiting); + chartProcess.value.pushData(stats[props.domain].activeSincePrevTick); + chartActive.value.pushData(stats[props.domain].active); + chartDelayed.value.pushData(stats[props.domain].delayed); + chartWaiting.value.pushData(stats[props.domain].waiting); }; const onStatsLog = (statsLog) => { @@ -97,10 +97,10 @@ const onStatsLog = (statsLog) => { dataWaiting.push(stats[props.domain].waiting); } - chartProcess.setData(dataProcess); - chartActive.setData(dataActive); - chartDelayed.setData(dataDelayed); - chartWaiting.setData(dataWaiting); + chartProcess.value.setData(dataProcess); + chartActive.value.setData(dataActive); + chartDelayed.value.setData(dataDelayed); + chartWaiting.value.setData(dataWaiting); }; onMounted(() => { diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue index 62a8aa1e22..245c55f6e7 100644 --- a/packages/frontend/src/pages/admin/queue.vue +++ b/packages/frontend/src/pages/admin/queue.vue @@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref, computed } from 'vue'; import XQueue from './queue.chart.vue'; import XHeader from './_header_.vue'; import * as os from '@/os.js'; @@ -24,7 +25,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; -let tab = $ref('deliver'); +const tab = ref('deliver'); function clear() { os.confirm({ @@ -46,20 +47,20 @@ function promoteAllQueues() { }).then(({ canceled }) => { if (canceled) return; - os.apiWithDialog('admin/queue/promote', { type: tab }); + os.apiWithDialog('admin/queue/promote', { type: tab.value }); }); } -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ asFullButton: true, icon: 'ph-arrow-square-out ph-bold ph-lg', text: i18n.ts.dashboard, handler: () => { - window.open(config.url + '/queue', '_blank'); + window.open(config.url + '/queue', '_blank', 'noopener'); }, }]); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'deliver', title: 'Deliver', }, { diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue index 9b4a84d12f..b4d6490e09 100644 --- a/packages/frontend/src/pages/admin/relays.vue +++ b/packages/frontend/src/pages/admin/relays.vue @@ -24,14 +24,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let relays: any[] = $ref([]); +const relays = ref<any[]>([]); async function addRelay() { const { canceled, result: inbox } = await os.inputText({ @@ -67,20 +67,20 @@ function remove(inbox: string) { function refresh() { os.api('admin/relays/list').then((relayList: any) => { - relays = relayList; + relays.value = relayList; }); } refresh(); -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ asFullButton: true, icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.addRelay, handler: addRelay, }]); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.relays, diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue index a982b41e71..b3c06454ae 100644 --- a/packages/frontend/src/pages/admin/roles.edit.vue +++ b/packages/frontend/src/pages/admin/roles.edit.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import { v4 as uuid } from 'uuid'; import XHeader from './_header_.vue'; import XEditor from './roles.editor.vue'; @@ -39,17 +39,17 @@ const props = defineProps<{ id?: string; }>(); -let role = $ref(null); -let data = $ref(null); +const role = ref(null); +const data = ref(null); if (props.id) { - role = await os.api('admin/roles/show', { + role.value = await os.api('admin/roles/show', { roleId: props.id, }); - data = role; + data.value = role.value; } else { - data = { + data.value = { name: 'New Role', description: '', isAdministrator: false, @@ -69,24 +69,24 @@ if (props.id) { async function save() { rolesCache.delete(); - if (role) { + if (role.value) { os.apiWithDialog('admin/roles/update', { - roleId: role.id, - ...data, + roleId: role.value.id, + ...data.value, }); - router.push('/admin/roles/' + role.id); + router.push('/admin/roles/' + role.value.id); } else { const created = await os.apiWithDialog('admin/roles/create', { - ...data, + ...data.value, }); router.push('/admin/roles/' + created.id); } } -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(computed(() => role ? { - title: i18n.ts._role.edit + ': ' + role.name, +definePageMetadata(computed(() => role.value ? { + title: i18n.ts._role.edit + ': ' + role.value.name, icon: 'ph-seal-check ph-bold ph-lg', } : { title: i18n.ts._role.new, diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index efdf1ff4f8..164510fd24 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -318,7 +318,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkRange> </div> </MkFolder> - + <MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])"> <template #label>{{ i18n.ts._role._options.canSearchNotes }}</template> <template #suffix> @@ -571,13 +571,33 @@ SPDX-License-Identifier: AGPL-3.0-only </MkRange> </div> </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])"> + <template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template> + <template #suffix> + <span v-if="role.policies.avatarDecorationLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.avatarDecorationLimit.value }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.avatarDecorationLimit)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.avatarDecorationLimit.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkInput v-model="role.policies.avatarDecorationLimit.value" type="number" :min="0"> + <template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template> + </MkInput> + <MkRange v-model="role.policies.avatarDecorationLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> </div> </FormSlot> </div> </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, ref, computed } from 'vue'; import { throttle } from 'throttle-debounce'; import RolesEditorFormula from './RolesEditorFormula.vue'; import MkInput from '@/components/MkInput.vue'; @@ -589,7 +609,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkRange from '@/components/MkRange.vue'; import FormSlot from '@/components/form/slot.vue'; import { i18n } from '@/i18n.js'; -import { ROLE_POLICIES } from '@/const'; +import { ROLE_POLICIES } from '@/const.js'; import { instance } from '@/instance.js'; import { deepClone } from '@/scripts/clone.js'; @@ -602,12 +622,12 @@ const props = defineProps<{ readonly?: boolean; }>(); -let role = $ref(deepClone(props.modelValue)); +const role = ref(deepClone(props.modelValue)); // fill missing policy for (const ROLE_POLICY of ROLE_POLICIES) { - if (role.policies[ROLE_POLICY] == null) { - role.policies[ROLE_POLICY] = { + if (role.value.policies[ROLE_POLICY] == null) { + role.value.policies[ROLE_POLICY] = { useDefault: true, priority: 0, value: instance.policies[ROLE_POLICY], @@ -615,15 +635,15 @@ for (const ROLE_POLICY of ROLE_POLICIES) { } } -let rolePermission = $computed({ - get: () => role.isAdministrator ? 'administrator' : role.isModerator ? 'moderator' : 'normal', +const rolePermission = computed({ + get: () => role.value.isAdministrator ? 'administrator' : role.value.isModerator ? 'moderator' : 'normal', set: (val) => { - role.isAdministrator = val === 'administrator'; - role.isModerator = val === 'moderator'; + role.value.isAdministrator = val === 'administrator'; + role.value.isModerator = val === 'moderator'; }, }); -let q = $ref(''); +const q = ref(''); function getPriorityIcon(option) { if (option.priority === 2) return 'ph-arrow-up ph-bold ph-lg'; @@ -632,32 +652,32 @@ function getPriorityIcon(option) { } function matchQuery(keywords: string[]): boolean { - if (q.trim().length === 0) return true; - return keywords.some(keyword => keyword.toLowerCase().includes(q.toLowerCase())); + if (q.value.trim().length === 0) return true; + return keywords.some(keyword => keyword.toLowerCase().includes(q.value.toLowerCase())); } const save = throttle(100, () => { const data = { - name: role.name, - description: role.description, - color: role.color === '' ? null : role.color, - iconUrl: role.iconUrl === '' ? null : role.iconUrl, - displayOrder: role.displayOrder, - target: role.target, - condFormula: role.condFormula, - isAdministrator: role.isAdministrator, - isModerator: role.isModerator, - isPublic: role.isPublic, - isExplorable: role.isExplorable, - asBadge: role.asBadge, - canEditMembersByModerator: role.canEditMembersByModerator, - policies: role.policies, + name: role.value.name, + description: role.value.description, + color: role.value.color === '' ? null : role.value.color, + iconUrl: role.value.iconUrl === '' ? null : role.value.iconUrl, + displayOrder: role.value.displayOrder, + target: role.value.target, + condFormula: role.value.condFormula, + isAdministrator: role.value.isAdministrator, + isModerator: role.value.isModerator, + isPublic: role.value.isPublic, + isExplorable: role.value.isExplorable, + asBadge: role.value.asBadge, + canEditMembersByModerator: role.value.canEditMembersByModerator, + policies: role.value.policies, }; emit('update:modelValue', data); }); -watch($$(role), save, { deep: true }); +watch(role, save, { deep: true }); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index 953db11a1a..92818cc3de 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, reactive } from 'vue'; +import { computed, reactive, ref } from 'vue'; import XHeader from './_header_.vue'; import XEditor from './roles.editor.vue'; import MkFolder from '@/components/MkFolder.vue'; @@ -73,7 +73,7 @@ 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, { Paging } from '@/components/MkPagination.vue'; +import MkPagination from '@/components/MkPagination.vue'; import { infoImageUrl } from '@/instance.js'; const router = useRouter(); @@ -90,7 +90,7 @@ const usersPagination = { })), }; -let expandedItems = $ref([]); +const expandedItems = ref([]); const role = reactive(await os.api('admin/roles/show', { roleId: props.id, @@ -160,16 +160,16 @@ async function unassign(user, ev) { } async function toggleItem(item) { - if (expandedItems.includes(item.id)) { - expandedItems = expandedItems.filter(x => x !== item.id); + if (expandedItems.value.includes(item.id)) { + expandedItems.value = expandedItems.value.filter(x => x !== item.id); } else { - expandedItems.push(item.id); + expandedItems.value.push(item.id); } } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(computed(() => ({ title: i18n.ts.role + ': ' + role.name, diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 7fedb87d41..07cce6d9a5 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -211,19 +211,26 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSwitch> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])"> + <template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template> + <template #suffix>{{ policies.avatarDecorationLimit }}</template> + <MkInput v-model="policies.avatarDecorationLimit" type="number" :min="0"> + </MkInput> + </MkFolder> + <MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton> </div> </MkFolder> <MkButton primary rounded @click="create"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts._role.new }}</MkButton> <div class="_gaps_s"> <MkFoldableSection> - <template #header>Manual roles</template> + <template #header>{{ i18n.ts._role.manualRoles }}</template> <div class="_gaps_s"> <MkRolePreview v-for="role in roles.filter(x => x.target === 'manual')" :key="role.id" :role="role" :forModeration="true"/> </div> </MkFoldableSection> <MkFoldableSection> - <template #header>Conditional roles</template> + <template #header>{{ i18n.ts._role.conditionalRoles }}</template> <div class="_gaps_s"> <MkRolePreview v-for="role in roles.filter(x => x.target === 'conditional')" :key="role.id" :role="role" :forModeration="true"/> </div> @@ -278,9 +285,9 @@ function create() { router.push('/admin/roles/new'); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(computed(() => ({ title: i18n.ts.roles, diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index f02fa1024d..95f4d2b20c 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -30,6 +30,13 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="enableActiveEmailValidation" @update:modelValue="save"> <template #label>Enable</template> </MkSwitch> + <MkSwitch v-model="enableVerifymailApi" @update:modelValue="save"> + <template #label>Use Verifymail API</template> + </MkSwitch> + <MkInput v-model="verifymailAuthKey" @update:modelValue="save"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>Verifymail API Auth Key</template> + </MkInput> </div> </MkFolder> @@ -64,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import XBotProtection from './bot-protection.vue'; import XHeader from './_header_.vue'; import MkFolder from '@/components/MkFolder.vue'; @@ -79,36 +86,42 @@ import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let summalyProxy: string = $ref(''); -let enableHcaptcha: boolean = $ref(false); -let enableRecaptcha: boolean = $ref(false); -let enableTurnstile: boolean = $ref(false); -let enableIpLogging: boolean = $ref(false); -let enableActiveEmailValidation: boolean = $ref(false); +const summalyProxy = ref<string>(''); +const enableHcaptcha = 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); async function init() { const meta = await os.api('admin/meta'); - summalyProxy = meta.summalyProxy; - enableHcaptcha = meta.enableHcaptcha; - enableRecaptcha = meta.enableRecaptcha; - enableTurnstile = meta.enableTurnstile; - enableIpLogging = meta.enableIpLogging; - enableActiveEmailValidation = meta.enableActiveEmailValidation; + summalyProxy.value = meta.summalyProxy; + enableHcaptcha.value = meta.enableHcaptcha; + enableRecaptcha.value = meta.enableRecaptcha; + enableTurnstile.value = meta.enableTurnstile; + enableIpLogging.value = meta.enableIpLogging; + enableActiveEmailValidation.value = meta.enableActiveEmailValidation; + enableVerifymailApi.value = meta.enableVerifymailApi; + verifymailAuthKey.value = meta.verifymailAuthKey; } function save() { os.apiWithDialog('admin/update-meta', { - summalyProxy, - enableIpLogging, - enableActiveEmailValidation, + summalyProxy: summalyProxy.value, + enableIpLogging: enableIpLogging.value, + enableActiveEmailValidation: enableActiveEmailValidation.value, + enableVerifymailApi: enableVerifymailApi.value, + verifymailAuthKey: verifymailAuthKey.value, }).then(() => { fetchInstance(); }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.security, diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue index 4302bde91d..6aecb43399 100644 --- a/packages/frontend/src/pages/admin/server-rules.vue +++ b/packages/frontend/src/pages/admin/server-rules.vue @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; +import { defineAsyncComponent, ref, computed } from 'vue'; import XHeader from './_header_.vue'; import * as os from '@/os.js'; import { fetchInstance, instance } from '@/instance.js'; @@ -52,20 +52,20 @@ import MkInput from '@/components/MkInput.vue'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); -let serverRules: string[] = $ref(instance.serverRules); +const serverRules = ref<string[]>(instance.serverRules); const save = async () => { await os.apiWithDialog('admin/update-meta', { - serverRules, + serverRules: serverRules.value, }); fetchInstance(); }; const remove = (index: number): void => { - serverRules.splice(index, 1); + serverRules.value.splice(index, 1); }; -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.serverRules, diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index db117d5061..07d7acf11c 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -148,7 +148,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; @@ -163,76 +163,76 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; -let name: string | null = $ref(null); -let shortName: string | null = $ref(null); -let description: string | null = $ref(null); -let maintainerName: string | null = $ref(null); -let maintainerEmail: string | null = $ref(null); -let impressumUrl: string | null = $ref(null); -let pinnedUsers: string = $ref(''); -let cacheRemoteFiles: boolean = $ref(false); -let cacheRemoteSensitiveFiles: boolean = $ref(false); -let enableServiceWorker: boolean = $ref(false); -let swPublicKey: any = $ref(null); -let swPrivateKey: any = $ref(null); -let enableFanoutTimeline: boolean = $ref(false); -let enableFanoutTimelineDbFallback: boolean = $ref(false); -let perLocalUserUserTimelineCacheMax: number = $ref(0); -let perRemoteUserUserTimelineCacheMax: number = $ref(0); -let perUserHomeTimelineCacheMax: number = $ref(0); -let perUserListTimelineCacheMax: number = $ref(0); -let notesPerOneAd: number = $ref(0); +const name = ref<string | null>(null); +const shortName = ref<string | null>(null); +const description = ref<string | null>(null); +const maintainerName = ref<string | null>(null); +const maintainerEmail = ref<string | null>(null); +const impressumUrl = ref<string | null>(null); +const pinnedUsers = ref<string>(''); +const cacheRemoteFiles = ref<boolean>(false); +const cacheRemoteSensitiveFiles = ref<boolean>(false); +const enableServiceWorker = ref<boolean>(false); +const swPublicKey = ref<any>(null); +const swPrivateKey = ref<any>(null); +const enableFanoutTimeline = ref<boolean>(false); +const enableFanoutTimelineDbFallback = ref<boolean>(false); +const perLocalUserUserTimelineCacheMax = ref<number>(0); +const perRemoteUserUserTimelineCacheMax = ref<number>(0); +const perUserHomeTimelineCacheMax = ref<number>(0); +const perUserListTimelineCacheMax = ref<number>(0); +const notesPerOneAd = ref<number>(0); async function init(): Promise<void> { const meta = await os.api('admin/meta'); - name = meta.name; - shortName = meta.shortName; - description = meta.description; - maintainerName = meta.maintainerName; - maintainerEmail = meta.maintainerEmail; - impressumUrl = meta.impressumUrl; - pinnedUsers = meta.pinnedUsers.join('\n'); - cacheRemoteFiles = meta.cacheRemoteFiles; - cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles; - enableServiceWorker = meta.enableServiceWorker; - swPublicKey = meta.swPublickey; - swPrivateKey = meta.swPrivateKey; - enableFanoutTimeline = meta.enableFanoutTimeline; - enableFanoutTimelineDbFallback = meta.enableFanoutTimelineDbFallback; - perLocalUserUserTimelineCacheMax = meta.perLocalUserUserTimelineCacheMax; - perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax; - perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax; - perUserListTimelineCacheMax = meta.perUserListTimelineCacheMax; - notesPerOneAd = meta.notesPerOneAd; + name.value = meta.name; + shortName.value = meta.shortName; + description.value = meta.description; + maintainerName.value = meta.maintainerName; + maintainerEmail.value = meta.maintainerEmail; + impressumUrl.value = meta.impressumUrl; + pinnedUsers.value = meta.pinnedUsers.join('\n'); + cacheRemoteFiles.value = meta.cacheRemoteFiles; + cacheRemoteSensitiveFiles.value = meta.cacheRemoteSensitiveFiles; + enableServiceWorker.value = meta.enableServiceWorker; + swPublicKey.value = meta.swPublickey; + swPrivateKey.value = meta.swPrivateKey; + enableFanoutTimeline.value = meta.enableFanoutTimeline; + enableFanoutTimelineDbFallback.value = meta.enableFanoutTimelineDbFallback; + perLocalUserUserTimelineCacheMax.value = meta.perLocalUserUserTimelineCacheMax; + perRemoteUserUserTimelineCacheMax.value = meta.perRemoteUserUserTimelineCacheMax; + perUserHomeTimelineCacheMax.value = meta.perUserHomeTimelineCacheMax; + perUserListTimelineCacheMax.value = meta.perUserListTimelineCacheMax; + notesPerOneAd.value = meta.notesPerOneAd; } async function save(): void { await os.apiWithDialog('admin/update-meta', { - name, - shortName: shortName === '' ? null : shortName, - description, - maintainerName, - maintainerEmail, - impressumUrl, - pinnedUsers: pinnedUsers.split('\n'), - cacheRemoteFiles, - cacheRemoteSensitiveFiles, - enableServiceWorker, - swPublicKey, - swPrivateKey, - enableFanoutTimeline, - enableFanoutTimelineDbFallback, - perLocalUserUserTimelineCacheMax, - perRemoteUserUserTimelineCacheMax, - perUserHomeTimelineCacheMax, - perUserListTimelineCacheMax, - notesPerOneAd, + name: name.value, + shortName: shortName.value === '' ? null : shortName.value, + description: description.value, + maintainerName: maintainerName.value, + maintainerEmail: maintainerEmail.value, + impressumUrl: impressumUrl.value, + pinnedUsers: pinnedUsers.value.split('\n'), + cacheRemoteFiles: cacheRemoteFiles.value, + cacheRemoteSensitiveFiles: cacheRemoteSensitiveFiles.value, + enableServiceWorker: enableServiceWorker.value, + swPublicKey: swPublicKey.value, + swPrivateKey: swPrivateKey.value, + enableFanoutTimeline: enableFanoutTimeline.value, + enableFanoutTimelineDbFallback: enableFanoutTimelineDbFallback.value, + perLocalUserUserTimelineCacheMax: perLocalUserUserTimelineCacheMax.value, + perRemoteUserUserTimelineCacheMax: perRemoteUserUserTimelineCacheMax.value, + perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value, + perUserListTimelineCacheMax: perUserListTimelineCacheMax.value, + notesPerOneAd: notesPerOneAd.value, }); fetchInstance(); } -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.general, diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue index 7f809ed9cc..1bc4eb4089 100644 --- a/packages/frontend/src/pages/admin/users.vue +++ b/packages/frontend/src/pages/admin/users.vue @@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, shallowRef, ref } from 'vue'; import XHeader from './_header_.vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; @@ -70,22 +70,22 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import { dateString } from '@/filters/date.js'; -let paginationComponent = $shallowRef<InstanceType<typeof MkPagination>>(); +const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>(); -let sort = $ref('+createdAt'); -let state = $ref('all'); -let origin = $ref('local'); -let searchUsername = $ref(''); -let searchHost = $ref(''); +const sort = ref('+createdAt'); +const state = ref('all'); +const origin = ref('local'); +const searchUsername = ref(''); +const searchHost = ref(''); const pagination = { endpoint: 'admin/show-users' as const, limit: 10, params: computed(() => ({ - sort: sort, - state: state, - origin: origin, - username: searchUsername, - hostname: searchHost, + sort: sort.value, + state: state.value, + origin: origin.value, + username: searchUsername.value, + hostname: searchHost.value, })), offsetMode: true, }; @@ -112,7 +112,7 @@ async function addUser() { username: username, password: password, }).then(res => { - paginationComponent.reload(); + paginationComponent.value.reload(); }); } @@ -120,7 +120,7 @@ function show(user) { os.pageWindow(`/admin/user/${user.id}`); } -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ icon: 'ph-magnifying-glass ph-bold ph-lg', text: i18n.ts.search, handler: searchUser, @@ -136,7 +136,7 @@ const headerActions = $computed(() => [{ handler: lookupUser, }]); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(computed(() => ({ title: i18n.ts.users, diff --git a/packages/frontend/src/pages/ads.vue b/packages/frontend/src/pages/ads.vue index bee3af39ef..9d508937af 100644 --- a/packages/frontend/src/pages/ads.vue +++ b/packages/frontend/src/pages/ads.vue @@ -16,8 +16,6 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; -import * as os from '@/os.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index 51e1efda53..705115abb0 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { ref, computed } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -90,9 +90,9 @@ async function read(announcement) { }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'current', title: i18n.ts.currentAnnouncements, icon: 'ph-fire ph-bold ph-lg', diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index dfba117799..c8a4c3f8dc 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref, shallowRef } from 'vue'; import MkTimeline from '@/components/MkTimeline.vue'; import { scroll } from '@/scripts/scroll.js'; import * as os from '@/os.js'; @@ -38,20 +38,20 @@ const props = defineProps<{ antennaId: string; }>(); -let antenna = $ref(null); -let queue = $ref(0); -let rootEl = $shallowRef<HTMLElement>(); -let tlEl = $shallowRef<InstanceType<typeof MkTimeline>>(); -const keymap = $computed(() => ({ +const antenna = ref(null); +const queue = ref(0); +const rootEl = shallowRef<HTMLElement>(); +const tlEl = shallowRef<InstanceType<typeof MkTimeline>>(); +const keymap = computed(() => ({ 't': focus, })); function queueUpdated(q) { - queue = q; + queue.value = q; } function top() { - scroll(rootEl, { top: 0 }); + scroll(rootEl.value, { top: 0 }); } async function timetravel() { @@ -60,7 +60,7 @@ async function timetravel() { }); if (canceled) return; - tlEl.timetravel(date); + tlEl.value.timetravel(date); } function settings() { @@ -68,16 +68,16 @@ function settings() { } function focus() { - tlEl.focus(); + tlEl.value.focus(); } watch(() => props.antennaId, async () => { - antenna = await os.api('antennas/show', { + antenna.value = await os.api('antennas/show', { antennaId: props.antennaId, }); }, { immediate: true }); -const headerActions = $computed(() => antenna ? [{ +const headerActions = computed(() => antenna.value ? [{ icon: 'ph-calendar ph-bold ph-lg', text: i18n.ts.jumpToSpecifiedDate, handler: timetravel, @@ -87,10 +87,10 @@ const headerActions = $computed(() => antenna ? [{ handler: settings, }] : []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(computed(() => antenna ? { - title: antenna.name, +definePageMetadata(computed(() => antenna.value ? { + title: antenna.value.name, icon: 'ph-flying-saucer ph-bold ph-lg', } : null)); </script> diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue index 93ecbdf0cc..946ff3b7ba 100644 --- a/packages/frontend/src/pages/api-console.vue +++ b/packages/frontend/src/pages/api-console.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { ref, computed } from 'vue'; import JSON5 from 'json5'; import { Endpoints } from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; @@ -83,9 +83,9 @@ function onEndpointChange() { }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: 'API console', diff --git a/packages/frontend/src/pages/auth.form.vue b/packages/frontend/src/pages/auth.form.vue index 3f6f58df55..8a17e5895d 100644 --- a/packages/frontend/src/pages/auth.form.vue +++ b/packages/frontend/src/pages/auth.form.vue @@ -20,14 +20,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { computed } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ - session: Misskey.entities.AuthSession; + session: Misskey.entities.AuthSessionShowResponse; }>(); const emit = defineEmits<{ @@ -35,11 +35,11 @@ const emit = defineEmits<{ (event: 'denied'): void; }>(); -const app = $computed(() => props.session.app); +const app = computed(() => props.session.app); -const name = $computed(() => { +const name = computed(() => { const el = document.createElement('div'); - el.textContent = app.name; + el.textContent = app.value.name; return el.innerHTML; }); diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue index d6c12e7883..d97e89842d 100644 --- a/packages/frontend/src/pages/auth.vue +++ b/packages/frontend/src/pages/auth.vue @@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import XForm from './auth.form.vue'; import MkSignin from '@/components/MkSignin.vue'; @@ -58,33 +58,33 @@ const props = defineProps<{ const getUrlParams = () => window.location.search .substring(1) - .split("&") + .split('&') .reduce((result, query) => { - const [k, v] = query.split("="); + const [k, v] = query.split('='); result[k] = decodeURI(v); return result; }, {}); -let state = $ref<'waiting' | 'accepted' | 'fetch-session-error' | 'denied' | null>(null); -let session = $ref<Misskey.entities.AuthSession | null>(null); +const state = ref<'waiting' | 'accepted' | 'fetch-session-error' | 'denied' | null>(null); +const session = ref<Misskey.entities.AuthSessionShowResponse | null>(null); function accepted() { - state = 'accepted'; + state.value = 'accepted'; const isMastodon = !!getUrlParams().mastodon; - if (session && session.app.callbackUrl && isMastodon) { + if (session.value && session.value.app.callbackUrl && isMastodon) { const redirectUri = decodeURIComponent(getUrlParams().redirect_uri); - if (!session.app.callbackUrl.includes('elk.zone') && !session.app.callbackUrl.split("\n").includes(redirectUri)) { - state = "fetch-session-error"; - throw new Error("Callback URI doesn't match registered app"); + if (!session.value.app.callbackUrl.includes('elk.zone') && !session.value.app.callbackUrl.split('\n').includes(redirectUri)) { + state.value = 'fetch-session-error'; + throw new Error('Callback URI doesn\'t match registered app'); } - const callbackUrl = session.app.callbackUrl.includes('elk.zone') ? new URL(session.app.callbackUrl) : new URL(redirectUri); - callbackUrl.searchParams.append("code", session.token); - if (getUrlParams().state) callbackUrl.searchParams.append("state", getUrlParams().state); + const callbackUrl = session.value.app.callbackUrl.includes('elk.zone') ? new URL(session.value.app.callbackUrl) : new URL(redirectUri); + callbackUrl.searchParams.append('code', session.value.token); + if (getUrlParams().state) callbackUrl.searchParams.append('state', getUrlParams().state); location.href = callbackUrl.toString(); - } else if (session && session.app.callbackUrl) { - const url = new URL(session.app.callbackUrl); + } else if (session.value && session.value.app.callbackUrl) { + const url = new URL(session.value.app.callbackUrl); if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url'); - location.href = `${session.app.callbackUrl}?token=${session.token}`; + location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`; } } @@ -96,27 +96,27 @@ onMounted(async () => { if (!$i) return; try { - session = await os.api('auth/session/show', { + session.value = await os.api('auth/session/show', { token: props.token, }); // 既に連携していた場合 - if (session.app.isAuthorized) { + if (session.value.app.isAuthorized) { await os.api('auth/accept', { - token: session.token, + token: session.value.token, }); accepted(); } else { - state = 'waiting'; + state.value = 'waiting'; } } catch (err) { - state = 'fetch-session-error'; + state.value = 'fetch-session-error'; } }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts._auth.shareAccessTitle, diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue index 47faeb5940..8e2eb2df25 100644 --- a/packages/frontend/src/pages/avatar-decorations.vue +++ b/packages/frontend/src/pages/avatar-decorations.vue @@ -34,22 +34,19 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; 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 MkRadios from '@/components/MkRadios.vue'; -import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkFolder from '@/components/MkFolder.vue'; -let avatarDecorations: any[] = $ref([]); +const avatarDecorations = ref<any[]>([]); function add() { - avatarDecorations.unshift({ + avatarDecorations.value.unshift({ _id: Math.random().toString(36), id: null, name: '', @@ -64,7 +61,7 @@ function del(avatarDecoration) { text: i18n.t('deleteAreYouSure', { x: avatarDecoration.name }), }).then(({ canceled }) => { if (canceled) return; - avatarDecorations = avatarDecorations.filter(x => x !== avatarDecoration); + avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration); os.api('admin/avatar-decorations/delete', avatarDecoration); }); } @@ -80,20 +77,20 @@ async function save(avatarDecoration) { function load() { os.api('admin/avatar-decorations/list').then(_avatarDecorations => { - avatarDecorations = _avatarDecorations; + avatarDecorations.value = _avatarDecorations; }); } load(); -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ asFullButton: true, icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.add, handler: add, }]); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.avatarDecorations, diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index acff4211d1..5408536bb9 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.name }}</template> </MkInput> - <MkTextarea v-model="description"> + <MkTextarea v-model="description" mfmAutocomplete :mfmPreview="true"> <template #label>{{ i18n.ts.description }}</template> </MkTextarea> @@ -70,7 +70,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref, watch, defineAsyncComponent } from 'vue'; -import MkTextarea from '@/components/MkTextarea.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkColorInput from '@/components/MkColorInput.vue'; @@ -81,6 +80,7 @@ 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'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -90,22 +90,22 @@ const props = defineProps<{ channelId?: string; }>(); -let channel = $ref(null); -let name = $ref(null); -let description = $ref(null); -let bannerUrl = $ref<string | null>(null); -let bannerId = $ref<string | null>(null); -let color = $ref('#000'); -let isSensitive = $ref(false); -let allowRenoteToExternal = $ref(true); +const channel = ref(null); +const name = ref(null); +const description = ref(null); +const bannerUrl = ref<string | null>(null); +const bannerId = ref<string | null>(null); +const color = ref('#000'); +const isSensitive = ref(false); +const allowRenoteToExternal = ref(true); const pinnedNotes = ref([]); -watch(() => bannerId, async () => { - if (bannerId == null) { - bannerUrl = null; +watch(() => bannerId.value, async () => { + if (bannerId.value == null) { + bannerUrl.value = null; } else { - bannerUrl = (await os.api('drive/files/show', { - fileId: bannerId, + bannerUrl.value = (await os.api('drive/files/show', { + fileId: bannerId.value, })).url; } }); @@ -113,20 +113,20 @@ watch(() => bannerId, async () => { async function fetchChannel() { if (props.channelId == null) return; - channel = await os.api('channels/show', { + channel.value = await os.api('channels/show', { channelId: props.channelId, }); - name = channel.name; - description = channel.description; - bannerId = channel.bannerId; - bannerUrl = channel.bannerUrl; - isSensitive = channel.isSensitive; - pinnedNotes.value = channel.pinnedNoteIds.map(id => ({ + name.value = channel.value.name; + description.value = channel.value.description; + bannerId.value = channel.value.bannerId; + bannerUrl.value = channel.value.bannerUrl; + isSensitive.value = channel.value.isSensitive; + pinnedNotes.value = channel.value.pinnedNoteIds.map(id => ({ id, })); - color = channel.color; - allowRenoteToExternal = channel.allowRenoteToExternal; + color.value = channel.value.color; + allowRenoteToExternal.value = channel.value.allowRenoteToExternal; } fetchChannel(); @@ -150,13 +150,13 @@ function removePinnedNote(index: number) { function save() { const params = { - name: name, - description: description, - bannerId: bannerId, + name: name.value, + description: description.value, + bannerId: bannerId.value, pinnedNoteIds: pinnedNotes.value.map(x => x.id), - color: color, - isSensitive: isSensitive, - allowRenoteToExternal: allowRenoteToExternal, + color: color.value, + isSensitive: isSensitive.value, + allowRenoteToExternal: allowRenoteToExternal.value, }; if (props.channelId) { @@ -172,7 +172,7 @@ function save() { async function archive() { const { canceled } = await os.confirm({ type: 'warning', - title: i18n.t('channelArchiveConfirmTitle', { name: name }), + title: i18n.t('channelArchiveConfirmTitle', { name: name.value }), text: i18n.ts.channelArchiveConfirmDescription, }); @@ -188,17 +188,17 @@ async function archive() { function setBannerImage(evt) { selectFile(evt.currentTarget ?? evt.target, null).then(file => { - bannerId = file.id; + bannerId.value = file.id; }); } function removeBannerImage() { - bannerId = null; + bannerId.value = null; } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(computed(() => props.channelId ? { title: i18n.ts._channel.edit, diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 84463db0e1..a50965131f 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/> <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ph-star ph-bold ph-lg"></i></MkButton> <MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ph-star ph-bold ph-lg"></i></MkButton> - <div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" :class="$style.banner"> + <div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner"> <div :class="$style.bannerStatus"> <div><i class="ph-users ph-bold ph-lg ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> <div><i class="ph-pencil ph-bold ph-lg ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div> @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFoldableSection> <template #header><i class="ph-push-pin ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template> - <div v-if="channel.pinnedNotes.length > 0" class="_gaps"> + <div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps"> <MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/> </div> </MkFoldableSection> @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる --> <MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/> - <MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after"/> + <MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/> </div> <div v-else-if="tab === 'featured'"> <MkNotes :pagination="featuredPagination"/> @@ -68,7 +68,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref } from 'vue'; +import * as Misskey from 'misskey-js'; import MkPostForm from '@/components/MkPostForm.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import XChannelFollowButton from '@/components/MkChannelFollowButton.vue'; @@ -86,6 +87,10 @@ import { defaultStore } from '@/store.js'; import MkNote from '@/components/MkNote.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; +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'; const router = useRouter(); @@ -93,13 +98,13 @@ const props = defineProps<{ channelId: string; }>(); -let tab = $ref('overview'); -let channel = $ref(null); -let favorited = $ref(false); -let searchQuery = $ref(''); -let searchPagination = $ref(); -let searchKey = $ref(''); -const featuredPagination = $computed(() => ({ +const tab = ref('overview'); +const channel = ref<Misskey.entities.Channel | null>(null); +const favorited = ref(false); +const searchQuery = ref(''); +const searchPagination = ref(); +const searchKey = ref(''); +const featuredPagination = computed(() => ({ endpoint: 'notes/featured' as const, limit: 10, params: { @@ -108,89 +113,129 @@ const featuredPagination = $computed(() => ({ })); watch(() => props.channelId, async () => { - channel = await os.api('channels/show', { + channel.value = await os.api('channels/show', { channelId: props.channelId, }); - favorited = channel.isFavorited; - if (favorited || channel.isFollowing) { - tab = 'timeline'; + favorited.value = channel.value.isFavorited ?? false; + if (favorited.value || channel.value.isFollowing) { + tab.value = 'timeline'; + } + + if ((favorited.value || channel.value.isFollowing) && channel.value.lastNotedAt) { + const lastReadedAt: number = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.value.id}`) ?? 0; + const lastNotedAt = Date.parse(channel.value.lastNotedAt); + + if (lastNotedAt > lastReadedAt) { + miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.value.id}`, lastNotedAt); + } } }, { immediate: true }); function edit() { - router.push(`/channels/${channel.id}/edit`); + router.push(`/channels/${channel.value?.id}/edit`); } function openPostForm() { os.post({ - channel, + channel: channel.value, }); } function favorite() { + if (!channel.value) return; + os.apiWithDialog('channels/favorite', { - channelId: channel.id, + channelId: channel.value.id, }).then(() => { - favorited = true; + favorited.value = true; }); } async function unfavorite() { + if (!channel.value) return; + const confirm = await os.confirm({ type: 'warning', text: i18n.ts.unfavoriteConfirm, }); if (confirm.canceled) return; os.apiWithDialog('channels/unfavorite', { - channelId: channel.id, + channelId: channel.value.id, }).then(() => { - favorited = false; + favorited.value = false; }); } async function search() { - const query = searchQuery.toString().trim(); + if (!channel.value) return; + + const query = searchQuery.value.toString().trim(); if (query == null) return; - searchPagination = { + searchPagination.value = { endpoint: 'notes/search', limit: 10, params: { query: query, - channelId: channel.id, + channelId: channel.value.id, }, }; - searchKey = query; + searchKey.value = query; } -const headerActions = $computed(() => { - if (channel && channel.userId) { - const share = { +const headerActions = computed(() => { + if (channel.value && channel.value.userId) { + const headerItems: PageHeaderItem[] = []; + + headerItems.push({ icon: 'ph-share-network ph-bold ph-lg', - text: i18n.ts.share, + text: i18n.ts.copyUrl, handler: async (): Promise<void> => { - navigator.share({ - title: channel.name, - text: channel.description, - url: `${url}/channels/${channel.id}`, - }); + if (!channel.value) { + console.warn('failed to copy channel URL. channel.value is null.'); + return; + } + copyToClipboard(`${url}/channels/${channel.value.id}`); + os.success(); }, - }; + }); + + if (isSupportShare()) { + headerItems.push({ + icon: 'ph-share-network ph-bold ph-lg', + text: i18n.ts.share, + handler: async (): Promise<void> => { + if (!channel.value) { + console.warn('failed to share channel. channel.value is null.'); + return; + } + + navigator.share({ + title: channel.value.name, + text: channel.value.description ?? undefined, + url: `${url}/channels/${channel.value.id}`, + }); + }, + }); + } + + if (($i && $i.id === channel.value.userId) || iAmModerator) { + headerItems.push({ + icon: 'ph-gear ph-bold ph-lg', + text: i18n.ts.edit, + handler: edit, + }); + } - const canEdit = ($i && $i.id === channel.userId) || iAmModerator; - return canEdit ? [share, { - icon: 'ph-gear ph-bold ph-lg', - text: i18n.ts.edit, - handler: edit, - }] : [share]; + return headerItems.length > 0 ? headerItems : null; } else { return null; } }); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'overview', title: i18n.ts.overview, icon: 'ph-info ph-bold ph-lg', @@ -208,8 +253,8 @@ const headerTabs = $computed(() => [{ icon: 'ph-magnifying-glass ph-bold ph-lg', }]); -definePageMetadata(computed(() => channel ? { - title: channel.name, +definePageMetadata(computed(() => channel.value ? { + title: channel.value.name, icon: 'ph-television ph-bold ph-lg', } : null)); </script> diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue index 07c205a001..182703f9da 100644 --- a/packages/frontend/src/pages/channels.vue +++ b/packages/frontend/src/pages/channels.vue @@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted } from 'vue'; +import { computed, onMounted, ref } from 'vue'; import MkChannelPreview from '@/components/MkChannelPreview.vue'; import MkChannelList from '@/components/MkChannelList.vue'; import MkPagination from '@/components/MkPagination.vue'; @@ -69,15 +69,15 @@ const props = defineProps<{ type?: string; }>(); -let key = $ref(''); -let tab = $ref('featured'); -let searchQuery = $ref(''); -let searchType = $ref('nameAndDescription'); -let channelPagination = $ref(); +const key = ref(''); +const tab = ref('featured'); +const searchQuery = ref(''); +const searchType = ref('nameAndDescription'); +const channelPagination = ref(); onMounted(() => { - searchQuery = props.query ?? ''; - searchType = props.type ?? 'nameAndDescription'; + searchQuery.value = props.query ?? ''; + searchType.value = props.type ?? 'nameAndDescription'; }); const featuredPagination = { @@ -99,35 +99,35 @@ const ownedPagination = { }; async function search() { - const query = searchQuery.toString().trim(); + const query = searchQuery.value.toString().trim(); if (query == null) return; - const type = searchType.toString().trim(); + const type = searchType.value.toString().trim(); - channelPagination = { + channelPagination.value = { endpoint: 'channels/search', limit: 10, params: { - query: searchQuery, + query: searchQuery.value, type: type, }, }; - key = query + type; + key.value = query + type; } function create() { router.push('/channels/new'); } -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.create, handler: create, }]); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'search', title: i18n.ts.search, icon: 'ph-magnifying-glass ph-bold ph-lg', diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 37b7870dd4..041cc0a204 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, provide } from 'vue'; +import { computed, watch, provide, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkNotes from '@/components/MkNotes.vue'; import { $i } from '@/account.js'; @@ -36,13 +36,15 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { url } from '@/config.js'; import MkButton from '@/components/MkButton.vue'; import { clipsCache } from '@/cache'; +import { isSupportShare } from '@/scripts/navigator.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; const props = defineProps<{ clipId: string, }>(); -let clip: Misskey.entities.Clip = $ref<Misskey.entities.Clip>(); -let favorited = $ref(false); +const clip = ref<Misskey.entities.Clip | null>(null); +const favorited = ref(false); const pagination = { endpoint: 'clips/notes' as const, limit: 10, @@ -51,24 +53,24 @@ const pagination = { })), }; -const isOwned: boolean | null = $computed<boolean | null>(() => $i && clip && ($i.id === clip.userId)); +const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId)); watch(() => props.clipId, async () => { - clip = await os.api('clips/show', { + clip.value = await os.api('clips/show', { clipId: props.clipId, }); - favorited = clip.isFavorited; + favorited.value = clip.value.isFavorited; }, { immediate: true, }); -provide('currentClip', $$(clip)); +provide('currentClip', clip); function favorite() { os.apiWithDialog('clips/favorite', { clipId: props.clipId, }).then(() => { - favorited = true; + favorited.value = true; }); } @@ -81,50 +83,58 @@ async function unfavorite() { os.apiWithDialog('clips/unfavorite', { clipId: props.clipId, }).then(() => { - favorited = false; + favorited.value = false; }); } -const headerActions = $computed(() => clip && isOwned ? [{ +const headerActions = computed(() => clip.value && isOwned.value ? [{ icon: 'ph-pencil ph-bold ph-lg', text: i18n.ts.edit, handler: async (): Promise<void> => { - const { canceled, result } = await os.form(clip.name, { + const { canceled, result } = await os.form(clip.value.name, { name: { type: 'string', label: i18n.ts.name, - default: clip.name, + default: clip.value.name, }, description: { type: 'string', required: false, multiline: true, + treatAsMfm: true, label: i18n.ts.description, - default: clip.description, + default: clip.value.description, }, isPublic: { type: 'boolean', label: i18n.ts.public, - default: clip.isPublic, + default: clip.value.isPublic, }, }); if (canceled) return; os.apiWithDialog('clips/update', { - clipId: clip.id, + clipId: clip.value.id, ...result, }); clipsCache.delete(); }, -}, ...(clip.isPublic ? [{ +}, ...(clip.value.isPublic ? [{ + icon: 'ph-share-network ph-bold ph-lg', + text: i18n.ts.copyUrl, + handler: async (): Promise<void> => { + copyToClipboard(`${url}/clips/${clip.value.id}`); + os.success(); + }, +}] : []), ...(clip.value.isPublic && isSupportShare() ? [{ icon: 'ph-share-network ph-bold ph-lg', text: i18n.ts.share, handler: async (): Promise<void> => { navigator.share({ - title: clip.name, - text: clip.description, - url: `${url}/clips/${clip.id}`, + title: clip.value.name, + text: clip.value.description, + url: `${url}/clips/${clip.value.id}`, }); }, }] : []), { @@ -134,20 +144,20 @@ const headerActions = $computed(() => clip && isOwned ? [{ handler: async (): Promise<void> => { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('deleteAreYouSure', { x: clip.name }), + text: i18n.t('deleteAreYouSure', { x: clip.value.name }), }); if (canceled) return; await os.apiWithDialog('clips/delete', { - clipId: clip.id, + clipId: clip.value.id, }); clipsCache.delete(); }, }] : null); -definePageMetadata(computed(() => clip ? { - title: clip.name, +definePageMetadata(computed(() => clip.value ? { + title: clip.value.name, icon: 'ph-paperclip ph-bold ph-lg', } : null)); </script> diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 0c5825091a..9f68b6b485 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -80,7 +80,7 @@ import MkInput from '@/components/MkInput.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import FormSplit from '@/components/form/split.vue'; -import { selectFile, selectFiles } from '@/scripts/select-file.js'; +import { selectFile } from '@/scripts/select-file.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -155,7 +155,7 @@ const edit = (emoji) => { }, 'closed'); }; -const im = (emoji) => { +const importEmoji = (emoji) => { os.apiWithDialog('admin/emoji/copy', { emojiId: emoji.id, }); @@ -169,13 +169,13 @@ const remoteMenu = (emoji, ev: MouseEvent) => { { text: i18n.ts.import, icon: 'ph-plus ph-bold ph-lg', - action: () => { im(emoji); }, + action: () => { importEmoji(emoji); }, }, { text: i18n.ts.delete, - icon: "ph-trash ph-bold ph-lg", + icon: 'ph-trash ph-bold ph-lg', action: () => { - os.apiWithDialog("admin/emoji/delete", { + os.apiWithDialog('admin/emoji/delete', { id: emoji.id, }); }, @@ -296,7 +296,7 @@ const delBulk = async () => { emojisPaginationComponent.value.reload(); }; -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ asFullButton: true, icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.addEmoji, @@ -306,7 +306,7 @@ const headerActions = $computed(() => [{ handler: menu, }]); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'local', title: i18n.ts.local, }, { diff --git a/packages/frontend/src/pages/drive.vue b/packages/frontend/src/pages/drive.vue index b92dc8b470..7c88abc167 100644 --- a/packages/frontend/src/pages/drive.vue +++ b/packages/frontend/src/pages/drive.vue @@ -10,19 +10,19 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import XDrive from '@/components/MkDrive.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let folder = $ref(null); +const folder = ref(null); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(computed(() => ({ - title: folder ? folder.name : i18n.ts.drive, + title: folder.value ? folder.value.name : i18n.ts.drive, icon: 'ph-cloud ph-bold ph-lg', hideHeader: true, }))); diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index b4ae855d5d..ce6b5ae5f0 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkButton from '@/components/MkButton.vue'; @@ -85,29 +85,29 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { customEmojiCategories } from '@/custom-emojis.js'; import MkSwitch from '@/components/MkSwitch.vue'; -import { selectFile, selectFiles } from '@/scripts/select-file.js'; +import { selectFile } from '@/scripts/select-file.js'; import MkRolePreview from '@/components/MkRolePreview.vue'; const props = defineProps<{ emoji?: any, }>(); -let dialog = $ref(null); -let name: string = $ref(props.emoji ? props.emoji.name : ''); -let category: string = $ref(props.emoji ? props.emoji.category : ''); -let aliases: string = $ref(props.emoji ? props.emoji.aliases.join(' ') : ''); -let license: string = $ref(props.emoji ? (props.emoji.license ?? '') : ''); -let isSensitive = $ref(props.emoji ? props.emoji.isSensitive : false); -let localOnly = $ref(props.emoji ? props.emoji.localOnly : false); -let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); -let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]); -let file = $ref<Misskey.entities.DriveFile>(); +const dialog = ref(null); +const name = ref<string>(props.emoji ? props.emoji.name : ''); +const category = ref<string>(props.emoji ? props.emoji.category : ''); +const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : ''); +const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : ''); +const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false); +const localOnly = ref(props.emoji ? props.emoji.localOnly : false); +const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); +const rolesThatCanBeUsedThisEmojiAsReaction = ref([]); +const file = ref<Misskey.entities.DriveFile>(); -watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => { - rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); +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); }, { immediate: true }); -const imgUrl = computed(() => file ? file.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null); +const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null); const emit = defineEmits<{ (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void, @@ -115,42 +115,42 @@ const emit = defineEmits<{ }>(); async function changeImage(ev) { - file = await selectFile(ev.currentTarget ?? ev.target, null); - const candidate = file.name.replace(/\.(.+)$/, ''); + file.value = await selectFile(ev.currentTarget ?? ev.target, null); + const candidate = file.value.name.replace(/\.(.+)$/, ''); if (candidate.match(/^[a-z0-9_]+$/)) { - name = candidate; + name.value = candidate; } } async function addRole() { const roles = await os.api('admin/roles/list'); - const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id); + const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id); const { canceled, result: role } = await os.select({ items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })), }); if (canceled) return; - rolesThatCanBeUsedThisEmojiAsReaction.push(role); + rolesThatCanBeUsedThisEmojiAsReaction.value.push(role); } async function removeRole(role, ev) { - rolesThatCanBeUsedThisEmojiAsReaction = rolesThatCanBeUsedThisEmojiAsReaction.filter(x => x.id !== role.id); + rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id); } async function done() { const params = { - name, - category: category === '' ? null : category, - aliases: aliases.split(' ').filter(x => x !== ''), - license: license === '' ? null : license, - isSensitive, - localOnly, - roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id), + name: name.value, + category: category.value === '' ? null : category.value, + aliases: aliases.value.split(' ').filter(x => x !== ''), + license: license.value === '' ? null : license.value, + isSensitive: isSensitive.value, + localOnly: localOnly.value, + roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id), }; - if (file) { - params.fileId = file.id; + if (file.value) { + params.fileId = file.value.id; } if (props.emoji) { @@ -166,7 +166,7 @@ async function done() { }, }); - dialog.close(); + dialog.value.close(); } else { const created = await os.apiWithDialog('admin/emoji/add', params); @@ -174,14 +174,14 @@ async function done() { created: created, }); - dialog.close(); + dialog.value.close(); } } async function del() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: name }), + text: i18n.t('removeAreYouSure', { x: name.value }), }); if (canceled) return; @@ -191,7 +191,7 @@ async function del() { emit('done', { deleted: true, }); - dialog.close(); + dialog.value.close(); }); } </script> diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue index f787b12097..d94fe96fa2 100644 --- a/packages/frontend/src/pages/emojis.emoji.vue +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -46,7 +46,7 @@ function menu(ev) { os.apiGet('emoji', { name: props.emoji.name }).then(res => { os.alert({ type: 'info', - text: `License: ${res.license}`, + 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.featured.vue b/packages/frontend/src/pages/explore.featured.vue index a36d1b3bda..000371528e 100644 --- a/packages/frontend/src/pages/explore.featured.vue +++ b/packages/frontend/src/pages/explore.featured.vue @@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref } from 'vue'; import MkNotes from '@/components/MkNotes.vue'; import MkTab from '@/components/MkTab.vue'; import { i18n } from '@/i18n.js'; @@ -30,5 +31,5 @@ const paginationForPolls = { offsetMode: true, }; -let tab = $ref('notes'); +const tab = ref('notes'); </script> diff --git a/packages/frontend/src/pages/explore.roles.vue b/packages/frontend/src/pages/explore.roles.vue index 995ccd777c..929da19426 100644 --- a/packages/frontend/src/pages/explore.roles.vue +++ b/packages/frontend/src/pages/explore.roles.vue @@ -12,14 +12,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import MkRolePreview from '@/components/MkRolePreview.vue'; import * as os from '@/os.js'; -let roles = $ref(); +const roles = ref(); os.api('roles/list').then(res => { - roles = res.filter(x => x.target === 'manual').sort((a, b) => b.displayOrder - a.displayOrder); + 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 f7215def62..741a3bc219 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, ref, shallowRef, computed } from 'vue'; import MkUserList from '@/components/MkUserList.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkTab from '@/components/MkTab.vue'; @@ -74,16 +74,16 @@ const props = defineProps<{ tag?: string; }>(); -let origin = $ref('local'); -let tagsEl = $shallowRef<InstanceType<typeof MkFoldableSection>>(); -let tagsLocal = $ref([]); -let tagsRemote = $ref([]); +const origin = ref('local'); +const tagsEl = shallowRef<InstanceType<typeof MkFoldableSection>>(); +const tagsLocal = ref([]); +const tagsRemote = ref([]); watch(() => props.tag, () => { - if (tagsEl) tagsEl.toggleContent(props.tag == null); + if (tagsEl.value) tagsEl.value.toggleContent(props.tag == null); }); -const tagUsers = $computed(() => ({ +const tagUsers = computed(() => ({ endpoint: 'hashtags/users' as const, limit: 30, params: { @@ -127,13 +127,13 @@ os.api('hashtags/list', { attachedToLocalUserOnly: true, limit: 30, }).then(tags => { - tagsLocal = tags; + tagsLocal.value = tags; }); os.api('hashtags/list', { sort: '+attachedRemoteUsers', attachedToRemoteUserOnly: true, limit: 30, }).then(tags => { - tagsRemote = tags; + tagsRemote.value = tags; }); </script> diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue index 8b5af28b12..9693e26598 100644 --- a/packages/frontend/src/pages/explore.vue +++ b/packages/frontend/src/pages/explore.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref, shallowRef } from 'vue'; import XFeatured from './explore.featured.vue'; import XUsers from './explore.users.vue'; import XRoles from './explore.roles.vue'; @@ -36,16 +36,16 @@ const props = withDefaults(defineProps<{ initialTab: 'featured', }); -let tab = $ref(props.initialTab); -let tagsEl = $shallowRef<InstanceType<typeof MkFoldableSection>>(); +const tab = ref(props.initialTab); +const tagsEl = shallowRef<InstanceType<typeof MkFoldableSection>>(); watch(() => props.tag, () => { - if (tagsEl) tagsEl.toggleContent(props.tag == null); + if (tagsEl.value) tagsEl.value.toggleContent(props.tag == null); }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'featured', icon: 'ph-lightning ph-bold ph-lg', title: i18n.ts.featured, diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index ce25ad63a8..a4c2c0bc37 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts._play.summary }}</template> </MkTextarea> <MkButton primary @click="selectPreset">{{ i18n.ts.selectFromPresets }}<i class="ph-caret-down ph-bold ph-lg"></i></MkButton> - <MkTextarea v-model="script" class="_monospace" tall spellcheck="false"> + <MkCodeEditor v-model="script" lang="is"> <template #label>{{ i18n.ts._play.script }}</template> - </MkTextarea> + </MkCodeEditor> <div class="_buttons"> <MkButton primary @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> <MkButton @click="show"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.show }}</MkButton> @@ -34,12 +34,13 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.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'; @@ -363,79 +364,79 @@ const props = defineProps<{ id?: string; }>(); -let flash = $ref(null); -let visibility = $ref('public'); +const flash = ref(null); +const visibility = ref('public'); if (props.id) { - flash = await os.api('flash/show', { + flash.value = await os.api('flash/show', { flashId: props.id, }); } -let title = $ref(flash?.title ?? 'New Play'); -let summary = $ref(flash?.summary ?? ''); -let permissions = $ref(flash?.permissions ?? []); -let script = $ref(flash?.script ?? PRESET_DEFAULT); +const title = ref(flash.value?.title ?? 'New Play'); +const summary = ref(flash.value?.summary ?? ''); +const permissions = ref(flash.value?.permissions ?? []); +const script = ref(flash.value?.script ?? PRESET_DEFAULT); function selectPreset(ev: MouseEvent) { os.popupMenu([{ text: 'Omikuji', action: () => { - script = PRESET_OMIKUJI; + script.value = PRESET_OMIKUJI; }, }, { text: 'Shuffle', action: () => { - script = PRESET_SHUFFLE; + script.value = PRESET_SHUFFLE; }, }, { text: 'Quiz', action: () => { - script = PRESET_QUIZ; + script.value = PRESET_QUIZ; }, }, { text: 'Timeline viewer', action: () => { - script = PRESET_TIMELINE; + script.value = PRESET_TIMELINE; }, }], ev.currentTarget ?? ev.target); } async function save() { - if (flash) { + if (flash.value) { os.apiWithDialog('flash/update', { flashId: props.id, - title, - summary, - permissions, - script, - visibility, + title: title.value, + summary: summary.value, + permissions: permissions.value, + script: script.value, + visibility: visibility.value, }); } else { const created = await os.apiWithDialog('flash/create', { - title, - summary, - permissions, - script, + title: title.value, + summary: summary.value, + permissions: permissions.value, + script: script.value, }); router.push('/play/' + created.id + '/edit'); } } function show() { - if (flash == null) { + if (flash.value == null) { os.alert({ text: 'Please save', }); } else { - os.pageWindow(`/play/${flash.id}`); + os.pageWindow(`/play/${flash.value.id}`); } } async function del() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('deleteAreYouSure', { x: flash.title }), + text: i18n.t('deleteAreYouSure', { x: flash.value.title }), }); if (canceled) return; @@ -445,12 +446,12 @@ async function del() { router.push('/play'); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(computed(() => flash ? { - title: i18n.ts._play.edit + ': ' + flash.title, +definePageMetadata(computed(() => flash.value ? { + title: i18n.ts._play.edit + ': ' + flash.value.title, } : { title: i18n.ts._play.new, })); diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index 0b4dd13911..2b9346fcac 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import MkFlashPreview from '@/components/MkFlashPreview.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; @@ -48,7 +48,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; const router = useRouter(); -let tab = $ref('featured'); +const tab = ref('featured'); const featuredFlashsPagination = { endpoint: 'flash/featured' as const, @@ -67,13 +67,13 @@ function create() { router.push('/play/new'); } -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.create, handler: create, }]); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'featured', title: i18n.ts._play.featured, icon: 'ph-fire ph-bold ph-lg', diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index fd03ac6579..c7ffd5c966 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -18,7 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton> <MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton> <MkButton v-tooltip="i18n.ts.shareWithNote" class="button" rounded @click="shareWithNote"><i class="ph-repeat ph-bold ph-lg ti-fw"></i></MkButton> - <MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></MkButton> + <MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></MkButton> + <MkButton v-if="isSupportShare()" v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></MkButton> </div> </div> <div v-else :class="$style.ready"> @@ -56,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onDeactivated, onUnmounted, Ref, ref, watch } from 'vue'; +import { computed, onDeactivated, onUnmounted, Ref, ref, watch, shallowRef } from 'vue'; import { Interpreter, Parser, values } from '@syuilo/aiscript'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; @@ -70,45 +71,52 @@ import MkFolder from '@/components/MkFolder.vue'; import MkCode from '@/components/MkCode.vue'; import { defaultStore } from '@/store.js'; import { $i } from '@/account.js'; +import { isSupportShare } from '@/scripts/navigator.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; const props = defineProps<{ id: string; }>(); -let flash = $ref(null); -let error = $ref(null); +const flash = ref(null); +const error = ref(null); function fetchFlash() { - flash = null; + flash.value = null; os.api('flash/show', { flashId: props.id, }).then(_flash => { - flash = _flash; + flash.value = _flash; }).catch(err => { - error = err; + error.value = err; }); } +function copyLink() { + copyToClipboard(`${url}/play/${flash.value.id}`); + os.success(); +} + function share() { navigator.share({ - title: flash.title, - text: flash.summary, - url: `${url}/play/${flash.id}`, + title: flash.value.title, + text: flash.value.summary, + url: `${url}/play/${flash.value.id}`, }); } function shareWithNote() { os.post({ - initialText: `${flash.title} ${url}/play/${flash.id}`, + initialText: `${flash.value.title} ${url}/play/${flash.value.id}`, }); } function like() { os.apiWithDialog('flash/like', { - flashId: flash.id, + flashId: flash.value.id, }).then(() => { - flash.isLiked = true; - flash.likedCount++; + flash.value.isLiked = true; + flash.value.likedCount++; }); } @@ -119,10 +127,10 @@ async function unlike() { }); if (confirm.canceled) return; os.apiWithDialog('flash/unlike', { - flashId: flash.id, + flashId: flash.value.id, }).then(() => { - flash.isLiked = false; - flash.likedCount--; + flash.value.isLiked = false; + flash.value.likedCount--; }); } @@ -130,39 +138,35 @@ watch(() => props.id, fetchFlash, { immediate: true }); const parser = new Parser(); -let started = $ref(false); -let aiscript = $shallowRef<Interpreter | null>(null); +const started = ref(false); +const aiscript = shallowRef<Interpreter | null>(null); const root = ref<AsUiRoot>(); -const components: Ref<AsUiComponent>[] = $ref([]); +const components = ref<Ref<AsUiComponent>[]>([]); function start() { - started = true; + started.value = true; run(); } async function run() { - if (aiscript) aiscript.abort(); + if (aiscript.value) aiscript.value.abort(); - aiscript = new Interpreter({ + aiscript.value = new Interpreter({ ...createAiScriptEnv({ - storageKey: 'flash:' + flash.id, + storageKey: 'flash:' + flash.value.id, }), - ...registerAsUiLib(components, (_root) => { + ...registerAsUiLib(components.value, (_root) => { root.value = _root.value; }), - THIS_ID: values.STR(flash.id), - THIS_URL: values.STR(`${url}/play/${flash.id}`), + 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(({ canceled, result: a }) => { - if (canceled) { - ok(''); - } else { - ok(a); - } + }).then(({ result: a }) => { + ok(a ?? ''); }); }); }, @@ -176,7 +180,7 @@ async function run() { let ast; try { - ast = parser.parse(flash.script); + ast = parser.parse(flash.value.script); } catch (err) { os.alert({ type: 'error', @@ -185,7 +189,7 @@ async function run() { return; } try { - await aiscript.exec(ast); + await aiscript.value.exec(ast); } catch (err) { os.alert({ type: 'error', @@ -196,24 +200,24 @@ async function run() { } onDeactivated(() => { - if (aiscript) aiscript.abort(); + if (aiscript.value) aiscript.value.abort(); }); onUnmounted(() => { - if (aiscript) aiscript.abort(); + if (aiscript.value) aiscript.value.abort(); }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(computed(() => flash ? { - title: flash.title, - avatar: flash.user, - path: `/play/${flash.id}`, +definePageMetadata(computed(() => flash.value ? { + title: flash.value.title, + avatar: flash.value.user, + path: `/play/${flash.value.id}`, share: { - title: flash.title, - text: flash.summary, + title: flash.value.title, + text: flash.value.summary, }, } : null)); </script> diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index 7546804a7b..d750664221 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -65,9 +65,9 @@ function reject(user) { }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(computed(() => ({ title: i18n.ts.followRequests, diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index 5daaca774c..ef6286f27f 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -56,38 +56,38 @@ const props = defineProps<{ postId?: string; }>(); -let init = $ref(null); -let files = $ref([]); -let description = $ref(null); -let title = $ref(null); -let isSensitive = $ref(false); +const init = ref(null); +const files = ref([]); +const description = ref(null); +const title = ref(null); +const isSensitive = ref(false); function selectFile(evt) { selectFiles(evt.currentTarget ?? evt.target, null).then(selected => { - files = files.concat(selected); + files.value = files.value.concat(selected); }); } function remove(file) { - files = files.filter(f => f.id !== file.id); + files.value = files.value.filter(f => f.id !== file.id); } async function save() { if (props.postId) { await os.apiWithDialog('gallery/posts/update', { postId: props.postId, - title: title, - description: description, - fileIds: files.map(file => file.id), - isSensitive: isSensitive, + title: title.value, + description: description.value, + fileIds: files.value.map(file => file.id), + isSensitive: isSensitive.value, }); router.push(`/gallery/${props.postId}`); } else { const created = await os.apiWithDialog('gallery/posts/create', { - title: title, - description: description, - fileIds: files.map(file => file.id), - isSensitive: isSensitive, + title: title.value, + description: description.value, + fileIds: files.value.map(file => file.id), + isSensitive: isSensitive.value, }); router.push(`/gallery/${created.id}`); } @@ -106,19 +106,19 @@ async function del() { } watch(() => props.postId, () => { - init = () => props.postId ? os.api('gallery/posts/show', { + init.value = () => props.postId ? os.api('gallery/posts/show', { postId: props.postId, }).then(post => { - files = post.files; - title = post.title; - description = post.description; - isSensitive = post.isSensitive; + files.value = post.files; + title.value = post.title; + description.value = post.description; + isSensitive.value = post.isSensitive; }) : Promise.resolve(null); }, { immediate: true }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(computed(() => props.postId ? { title: i18n.ts.edit, diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index f65380eee1..936d9b8393 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, ref, computed } from 'vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; @@ -61,9 +61,9 @@ const props = defineProps<{ tag?: string; }>(); -let tab = $ref('explore'); -let tags = $ref([]); -let tagsRef = $ref(); +const tab = ref('explore'); +const tags = ref([]); +const tagsRef = ref(); const recentPostsPagination = { endpoint: 'gallery/posts' as const, @@ -82,7 +82,7 @@ const likedPostsPagination = { limit: 5, }; -const tagUsersPagination = $computed(() => ({ +const tagUsersPagination = computed(() => ({ endpoint: 'hashtags/users' as const, limit: 30, params: { @@ -93,10 +93,10 @@ const tagUsersPagination = $computed(() => ({ })); watch(() => props.tag, () => { - if (tagsRef) tagsRef.tags.toggleContent(props.tag == null); + if (tagsRef.value) tagsRef.value.tags.toggleContent(props.tag == null); }); -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.create, handler: () => { @@ -104,7 +104,7 @@ const headerActions = $computed(() => [{ }, }]); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'explore', title: i18n.ts.gallery, icon: 'ph-images-square ph-bold ph-lg', diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index ae609c994c..f9fa691580 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -29,7 +29,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="other"> <button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ph-pencil ph-bold ph-lg ti-fw"></i></button> <button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat ph-bold ph-lg ti-fw"></i></button> - <button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button> + <button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button> + <button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button> </div> </div> <div class="user"> @@ -61,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import MkContainer from '@/components/MkContainer.vue'; @@ -74,6 +75,8 @@ 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'; const router = useRouter(); @@ -81,38 +84,43 @@ const props = defineProps<{ postId: string; }>(); -let post = $ref(null); -let error = $ref(null); +const post = ref(null); +const error = ref(null); const otherPostsPagination = { endpoint: 'users/gallery/posts' as const, limit: 6, params: computed(() => ({ - userId: post.user.id, + userId: post.value.user.id, })), }; function fetchPost() { - post = null; + post.value = null; os.api('gallery/posts/show', { postId: props.postId, }).then(_post => { - post = _post; + post.value = _post; }).catch(_error => { - error = _error; + error.value = _error; }); } +function copyLink() { + copyToClipboard(`${url}/gallery/${post.value.id}`); + os.success(); +} + function share() { navigator.share({ - title: post.title, - text: post.description, - url: `${url}/gallery/${post.id}`, + title: post.value.title, + text: post.value.description, + url: `${url}/gallery/${post.value.id}`, }); } function shareWithNote() { os.post({ - initialText: `${post.title} ${url}/gallery/${post.id}`, + initialText: `${post.value.title} ${url}/gallery/${post.value.id}`, }); } @@ -120,8 +128,8 @@ function like() { os.apiWithDialog('gallery/posts/like', { postId: props.postId, }).then(() => { - post.isLiked = true; - post.likedCount++; + post.value.isLiked = true; + post.value.likedCount++; }); } @@ -134,28 +142,28 @@ async function unlike() { os.apiWithDialog('gallery/posts/unlike', { postId: props.postId, }).then(() => { - post.isLiked = false; - post.likedCount--; + post.value.isLiked = false; + post.value.likedCount--; }); } function edit() { - router.push(`/gallery/${post.id}/edit`); + router.push(`/gallery/${post.value.id}/edit`); } watch(() => props.postId, fetchPost, { immediate: true }); -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ icon: 'ph-pencil ph-bold ph-lg', text: i18n.ts.edit, handler: edit, }]); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(computed(() => post ? { - title: post.title, - avatar: post.user, +definePageMetadata(computed(() => post.value ? { + title: post.value.title, + avatar: post.value.user, } : null)); </script> diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 668e4e61bf..3e1bc31df7 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -118,7 +118,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import MkChart from '@/components/MkChart.vue'; import MkObjectView from '@/components/MkObjectView.vue'; @@ -143,15 +143,15 @@ const props = defineProps<{ host: string; }>(); -let tab = $ref('overview'); -let chartSrc = $ref('instance-requests'); -let meta = $ref<Misskey.entities.AdminInstanceMetadata | null>(null); -let instance = $ref<Misskey.entities.Instance | null>(null); -let suspended = $ref(false); -let isBlocked = $ref(false); -let isSilenced = $ref(false); -let isNSFW = $ref(false); -let faviconUrl = $ref<string | null>(null); +const tab = ref('overview'); +const chartSrc = ref('instance-requests'); +const meta = ref<Misskey.entities.AdminMetaResponse | null>(null); +const instance = ref<Misskey.entities.FederationInstance | null>(null); +const suspended = ref(false); +const isBlocked = ref(false); +const isSilenced = ref(false); +const isNSFW = ref(false); +const faviconUrl = ref<string | null>(null); const usersPagination = { endpoint: iAmModerator ? 'admin/show-users' : 'users' as const, @@ -166,41 +166,42 @@ const usersPagination = { async function fetch(): Promise<void> { if (iAmAdmin) { - meta = await os.api('admin/meta'); + meta.value = await os.api('admin/meta'); } - instance = await os.api('federation/show-instance', { + instance.value = await os.api('federation/show-instance', { host: props.host, }); - suspended = instance.isSuspended; - isBlocked = instance.isBlocked; - isSilenced = instance.isSilenced; - isNSFW = instance.isNSFW; - faviconUrl = getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.iconUrl, 'preview'); + suspended.value = instance.value?.isSuspended ?? false; + isBlocked.value = instance.value?.isBlocked ?? false; + isSilenced.value = instance.value?.isSilenced ?? false; + isNSFW.value = instance.value?.isNSFW ?? false; + faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview'); } async function toggleBlock(): Promise<void> { - if (!meta) throw new Error('No meta?'); - if (!instance) throw new Error('No instance?'); - const { host } = instance; + 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', { - blockedHosts: isBlocked ? meta.blockedHosts.concat([host]) : meta.blockedHosts.filter(x => x !== host), + blockedHosts: isBlocked.value ? meta.value.blockedHosts.concat([host]) : meta.value.blockedHosts.filter(x => x !== host), }); } async function toggleSilenced(): Promise<void> { - if (!meta) throw new Error('No meta?'); - if (!instance) throw new Error('No instance?'); - const { host } = instance; + if (!meta.value) throw new Error('No meta?'); + if (!instance.value) throw new Error('No instance?'); + const { host } = instance.value; + const silencedHosts = meta.value.silencedHosts ?? []; await os.api('admin/update-meta', { - silencedHosts: isSilenced ? meta.silencedHosts.concat([host]) : meta.silencedHosts.filter(x => x !== host), + silencedHosts: isSilenced.value ? silencedHosts.concat([host]) : silencedHosts.filter(x => x !== host), }); } async function toggleSuspend(): Promise<void> { - if (!instance) throw new Error('No instance?'); + if (!instance.value) throw new Error('No instance?'); await os.api('admin/federation/update-instance', { - host: instance.host, - isSuspended: suspended, + host: instance.value.host, + isSuspended: suspended.value, }); } @@ -213,9 +214,9 @@ async function toggleNSFW(): Promise<void> { } function refreshMetadata(): void { - if (!instance) throw new Error('No instance?'); + if (!instance.value) throw new Error('No instance?'); os.api('admin/federation/refresh-remote-instance-metadata', { - host: instance.host, + host: instance.value.host, }); os.alert({ text: 'Refresh requested', @@ -224,15 +225,15 @@ function refreshMetadata(): void { fetch(); -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ text: `https://${props.host}`, icon: 'ph-arrow-square-out ph-bold ph-lg', handler: () => { - window.open(`https://${props.host}`, '_blank'); + window.open(`https://${props.host}`, '_blank', 'noopener'); }, }]); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'overview', title: i18n.ts.overview, icon: 'ph-info ph-bold ph-lg', diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue index a5bdc29dbb..6ac78a2068 100644 --- a/packages/frontend/src/pages/invite.vue +++ b/packages/frontend/src/pages/invite.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkPagination ref="pagingComponent" :pagination="pagination"> <template #default="{ items }"> <div class="_gaps_s"> - <MkInviteCode v-for="item in (items as Misskey.entities.Invite[])" :key="item.id" :invite="item" :onDeleted="deleted"/> + <MkInviteCode v-for="item in (items as Misskey.entities.InviteCode[])" :key="item.id" :invite="item" :onDeleted="deleted"/> </div> </template> </MkPagination> diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue index 1323c6b3b9..731ad9f2ae 100644 --- a/packages/frontend/src/pages/list.vue +++ b/packages/frontend/src/pages/list.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch, computed } from 'vue'; +import { watch, computed, ref } from 'vue'; import * as os from '@/os.js'; import { userPage } from '@/filters/user.js'; import { i18n } from '@/i18n.js'; @@ -47,41 +47,41 @@ const props = defineProps<{ listId: string; }>(); -let list = $ref(null); -let error = $ref(); -let users = $ref([]); +const list = ref(null); +const error = ref(); +const users = ref([]); function fetchList(): void { os.api('users/lists/show', { listId: props.listId, forPublic: true, }).then(_list => { - list = _list; + list.value = _list; os.api('users/show', { - userIds: list.userIds, + userIds: list.value.userIds, }).then(_users => { - users = _users; + users.value = _users; }); }).catch(err => { - error = err; + error.value = err; }); } function like() { os.apiWithDialog('users/lists/favorite', { - listId: list.id, + listId: list.value.id, }).then(() => { - list.isLiked = true; - list.likedCount++; + list.value.isLiked = true; + list.value.likedCount++; }); } function unlike() { os.apiWithDialog('users/lists/unfavorite', { - listId: list.id, + listId: list.value.id, }).then(() => { - list.isLiked = false; - list.likedCount--; + list.value.isLiked = false; + list.value.likedCount--; }); } @@ -90,17 +90,17 @@ async function create() { title: i18n.ts.enterListName, }); if (canceled) return; - await os.apiWithDialog('users/lists/create-from-public', { name: name, listId: list.id }); + await os.apiWithDialog('users/lists/create-from-public', { name: name, listId: list.value.id }); } watch(() => props.listId, fetchList, { immediate: true }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(computed(() => list ? { - title: list.name, +definePageMetadata(computed(() => list.value ? { + title: list.value.name, icon: 'ph-list ph-bold ph-lg', } : null)); </script> diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue index 1e3c627023..2b53b67ab3 100644 --- a/packages/frontend/src/pages/miauth.vue +++ b/packages/frontend/src/pages/miauth.vue @@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import MkSignin from '@/components/MkSignin.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; @@ -59,20 +59,20 @@ const props = defineProps<{ permission: string; // コンマ区切り }>(); -const _permissions = $computed(() => props.permission ? props.permission.split(',') : []); +const _permissions = computed(() => props.permission ? props.permission.split(',') : []); -let state = $ref<string | null>(null); +const state = ref<string | null>(null); async function accept(): Promise<void> { - state = 'waiting'; + state.value = 'waiting'; await os.api('miauth/gen-token', { session: props.session, name: props.name, iconUrl: props.icon, - permission: _permissions, + permission: _permissions.value, }); - state = 'accepted'; + state.value = 'accepted'; if (props.callback) { const cbUrl = new URL(props.callback); if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url'); @@ -82,16 +82,16 @@ async function accept(): Promise<void> { } function deny(): void { - state = 'denied'; + state.value = 'denied'; } function onLogin(res): void { login(res.i); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: 'MiAuth', diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue index d8613a0ba2..79b592dada 100644 --- a/packages/frontend/src/pages/my-antennas/create.vue +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref } from 'vue'; import XAntenna from './editor.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -18,7 +19,7 @@ import { antennasCache } from '@/cache.js'; const router = useRouter(); -let draft = $ref({ +const draft = ref({ name: '', src: 'all', userListId: null, diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue index 86410121d3..a325eb9b0f 100644 --- a/packages/frontend/src/pages/my-antennas/edit.vue +++ b/packages/frontend/src/pages/my-antennas/edit.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref } from 'vue'; import XAntenna from './editor.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; @@ -19,7 +20,7 @@ import { antennasCache } from '@/cache'; const router = useRouter(); -let antenna: any = $ref(null); +const antenna = ref<any>(null); const props = defineProps<{ antennaId: string @@ -31,7 +32,7 @@ function onAntennaUpdated() { } os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => { - antenna = antennaResponse; + antenna.value = antennaResponse; }); definePageMetadata({ diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue index ad60ba0a3c..5eacf15efb 100644 --- a/packages/frontend/src/pages/my-antennas/editor.vue +++ b/packages/frontend/src/pages/my-antennas/editor.vue @@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -69,38 +69,38 @@ const emit = defineEmits<{ (ev: 'deleted'): void, }>(); -let name: string = $ref(props.antenna.name); -let src: string = $ref(props.antenna.src); -let userListId: any = $ref(props.antenna.userListId); -let users: string = $ref(props.antenna.users.join('\n')); -let keywords: string = $ref(props.antenna.keywords.map(x => x.join(' ')).join('\n')); -let excludeKeywords: string = $ref(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n')); -let caseSensitive: boolean = $ref(props.antenna.caseSensitive); -let localOnly: boolean = $ref(props.antenna.localOnly); -let withReplies: boolean = $ref(props.antenna.withReplies); -let withFile: boolean = $ref(props.antenna.withFile); -let notify: boolean = $ref(props.antenna.notify); -let userLists: any = $ref(null); +const name = ref<string>(props.antenna.name); +const src = ref<string>(props.antenna.src); +const userListId = ref<any>(props.antenna.userListId); +const users = ref<string>(props.antenna.users.join('\n')); +const keywords = ref<string>(props.antenna.keywords.map(x => x.join(' ')).join('\n')); +const excludeKeywords = ref<string>(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n')); +const caseSensitive = ref<boolean>(props.antenna.caseSensitive); +const localOnly = ref<boolean>(props.antenna.localOnly); +const withReplies = ref<boolean>(props.antenna.withReplies); +const withFile = ref<boolean>(props.antenna.withFile); +const notify = ref<boolean>(props.antenna.notify); +const userLists = ref<any>(null); -watch(() => src, async () => { - if (src === 'list' && userLists === null) { - userLists = await os.api('users/lists/list'); +watch(() => src.value, async () => { + if (src.value === 'list' && userLists.value === null) { + userLists.value = await os.api('users/lists/list'); } }); async function saveAntenna() { const antennaData = { - name, - src, - userListId, - withReplies, - withFile, - notify, - caseSensitive, - localOnly, - users: users.trim().split('\n').map(x => x.trim()), - keywords: keywords.trim().split('\n').map(x => x.trim().split(' ')), - excludeKeywords: excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')), + name: name.value, + src: src.value, + userListId: userListId.value, + withReplies: withReplies.value, + withFile: withFile.value, + notify: notify.value, + caseSensitive: caseSensitive.value, + localOnly: localOnly.value, + users: users.value.trim().split('\n').map(x => x.trim()), + keywords: keywords.value.trim().split('\n').map(x => x.trim().split(' ')), + excludeKeywords: excludeKeywords.value.trim().split('\n').map(x => x.trim().split(' ')), }; if (props.antenna.id == null) { @@ -130,9 +130,9 @@ async function deleteAntenna() { function addUser() { os.selectUser().then(user => { - users = users.trim(); - users += '\n@' + Misskey.acct.toString(user as any); - users = users.trim(); + users.value = users.value.trim(); + users.value += '\n@' + Misskey.acct.toString(user as any); + users.value = users.value.trim(); }); } </script> diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue index 5c6432b80d..2a5fe07957 100644 --- a/packages/frontend/src/pages/my-antennas/index.vue +++ b/packages/frontend/src/pages/my-antennas/index.vue @@ -28,14 +28,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onActivated } from 'vue'; +import { onActivated, computed } from 'vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { antennasCache } from '@/cache'; import { infoImageUrl } from '@/instance.js'; -const antennas = $computed(() => antennasCache.value.value ?? []); +const antennas = computed(() => antennasCache.value.value ?? []); function fetch() { antennasCache.fetch(); @@ -43,7 +43,7 @@ function fetch() { fetch(); -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ asFullButton: true, icon: 'ph-arrows-counter-clockwise ph-bold ph-lg', text: i18n.ts.reload, @@ -53,7 +53,7 @@ const headerActions = $computed(() => [{ }, }]); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.manageAntennas, diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index eb84112c69..31ccf9ce2d 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, ref, shallowRef, computed } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import MkClipPreview from '@/components/MkClipPreview.vue'; @@ -41,13 +41,13 @@ const pagination = { limit: 10, }; -let tab = $ref('my'); -let favorites = $ref(); +const tab = ref('my'); +const favorites = ref(); -const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>(); +const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); -watch($$(tab), async () => { - favorites = await os.api('clips/my-favorites'); +watch(tab, async () => { + favorites.value = await os.api('clips/my-favorites'); }); async function create() { @@ -60,6 +60,7 @@ async function create() { type: 'string', required: false, multiline: true, + treatAsMfm: true, label: i18n.ts.description, }, isPublic: { @@ -74,20 +75,20 @@ async function create() { clipsCache.delete(); - pagingComponent.reload(); + pagingComponent.value.reload(); } function onClipCreated() { - pagingComponent.reload(); + pagingComponent.value.reload(); } function onClipDeleted() { - pagingComponent.reload(); + pagingComponent.value.reload(); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'my', title: i18n.ts.myClips, icon: 'ph-paperclip ph-bold ph-lg', diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue index 147eaa12c3..b0d750b218 100644 --- a/packages/frontend/src/pages/my-lists/index.vue +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onActivated } from 'vue'; +import { onActivated, computed } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkAvatars from '@/components/MkAvatars.vue'; import * as os from '@/os.js'; @@ -39,7 +39,7 @@ import { userListsCache } from '@/cache'; import { infoImageUrl } from '@/instance.js'; import { $i } from '@/account.js'; -const items = $computed(() => userListsCache.value.value ?? []); +const items = computed(() => userListsCache.value.value ?? []); function fetch() { userListsCache.fetch(); @@ -57,7 +57,7 @@ async function create() { fetch(); } -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ asFullButton: true, icon: 'ph-arrows-counter-clockwise ph-bold ph-lg', text: i18n.ts.reload, @@ -67,7 +67,7 @@ const headerActions = $computed(() => [{ }, }]); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.manageLists, diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index 5c6e418264..df9cdb0fce 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -68,7 +68,7 @@ import MkInput from '@/components/MkInput.vue'; import { userListsCache } from '@/cache.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; -import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import MkPagination from '@/components/MkPagination.vue'; const { enableInfiniteScroll, @@ -79,7 +79,7 @@ const props = defineProps<{ }>(); const paginationEl = ref<InstanceType<typeof MkPagination>>(); -let list = $ref<Misskey.entities.UserList | null>(null); +const list = ref<Misskey.entities.UserList | null>(null); const isPublic = ref(false); const name = ref(''); const membershipsPagination = { @@ -94,17 +94,17 @@ function fetchList() { os.api('users/lists/show', { listId: props.listId, }).then(_list => { - list = _list; - name.value = list.name; - isPublic.value = list.isPublic; + list.value = _list; + name.value = list.value.name; + isPublic.value = list.value.isPublic; }); } function addUser() { os.selectUser().then(user => { - if (!list) return; + if (!list.value) return; os.apiWithDialog('users/lists/push', { - listId: list.id, + listId: list.value.id, userId: user.id, }).then(() => { paginationEl.value.reload(); @@ -118,9 +118,9 @@ async function removeUser(item, ev) { icon: 'ph-x ph-bold ph-lg', danger: true, action: async () => { - if (!list) return; + if (!list.value) return; os.api('users/lists/pull', { - listId: list.id, + listId: list.value.id, userId: item.userId, }).then(() => { paginationEl.value.removeItem(item.id); @@ -135,7 +135,7 @@ async function showMembershipMenu(item, ev) { icon: item.withReplies ? 'ph-envelope-open ph-bold ph-lg' : 'ph-envelope ph-bold ph-lg', action: async () => { os.api('users/lists/update-membership', { - listId: list.id, + listId: list.value.id, userId: item.userId, withReplies: !item.withReplies, }).then(() => { @@ -149,42 +149,42 @@ async function showMembershipMenu(item, ev) { } async function deleteList() { - if (!list) return; + if (!list.value) return; const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: list.name }), + text: i18n.t('removeAreYouSure', { x: list.value.name }), }); if (canceled) return; await os.apiWithDialog('users/lists/delete', { - listId: list.id, + listId: list.value.id, }); userListsCache.delete(); mainRouter.push('/my/lists'); } async function updateSettings() { - if (!list) return; + if (!list.value) return; await os.apiWithDialog('users/lists/update', { - listId: list.id, + listId: list.value.id, name: name.value, isPublic: isPublic.value, }); userListsCache.delete(); - list.name = name.value; - list.isPublic = isPublic.value; + list.value.name = name.value; + list.value.isPublic = isPublic.value; } watch(() => props.listId, fetchList, { immediate: true }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(computed(() => list ? { - title: list.name, +definePageMetadata(computed(() => list.value ? { + title: list.value.name, icon: 'ph-list ph-bold ph-lg', } : null)); </script> diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue index 410979bdc6..e8ba31395e 100644 --- a/packages/frontend/src/pages/not-found.vue +++ b/packages/frontend/src/pages/not-found.vue @@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { computed } from 'vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { pleaseLogin } from '@/scripts/please-login.js'; @@ -26,9 +27,9 @@ if (props.showLoginPopup) { pleaseLogin('/'); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.notFound, diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index 14f9ff3816..1e62ca9f61 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkNoteDetailed from '@/components/MkNoteDetailed.vue'; import MkNotes from '@/components/MkNotes.vue'; @@ -66,19 +66,19 @@ const props = defineProps<{ noteId: string; }>(); -let note = $ref<null | Misskey.entities.Note>(); -let clips = $ref(); -let showPrev = $ref(false); -let showNext = $ref(false); -let expandAllCws = $ref(false); -let error = $ref(); +const note = ref<null | Misskey.entities.Note>(); +const clips = ref(); +const showPrev = ref(false); +const showNext = ref(false); +const expandAllCws = ref(false); +const error = ref(); const prevPagination = { endpoint: 'users/notes' as const, limit: 10, - params: computed(() => note ? ({ - userId: note.userId, - untilId: note.id, + params: computed(() => note.value ? ({ + userId: note.value.userId, + untilId: note.value.id, }) : null), }; @@ -86,30 +86,30 @@ const nextPagination = { reversed: true, endpoint: 'users/notes' as const, limit: 10, - params: computed(() => note ? ({ - userId: note.userId, - sinceId: note.id, + params: computed(() => note.value ? ({ + userId: note.value.userId, + sinceId: note.value.id, }) : null), }; function fetchNote() { - showPrev = false; - showNext = false; - note = null; + showPrev.value = false; + showNext.value = false; + note.value = null; os.api('notes/show', { noteId: props.noteId, }).then(res => { - note = res; + note.value = res; // 古いノートは被クリップ数をカウントしていないので、2023-10-01以前のものは強制的にnotes/clipsを叩く - if (note.clippedCount > 0 || new Date(note.createdAt).getTime() < new Date('2023-10-01').getTime()) { + if (note.value.clippedCount > 0 || new Date(note.value.createdAt).getTime() < new Date('2023-10-01').getTime()) { os.api('notes/clips', { - noteId: note.id, + noteId: note.value.id, }).then((_clips) => { - clips = _clips; + clips.value = _clips; }); } }).catch(err => { - error = err; + error.value = err; }); } @@ -117,24 +117,24 @@ watch(() => props.noteId, fetchNote, { immediate: true, }); -const headerActions = $computed(() => note ? [ +const headerActions = computed(() => note.value ? [ { - icon: `${expandAllCws ? 'ph-eye' : 'ph-eye-slash'} ph-bold ph-lg`, - text: expandAllCws ? i18n.ts.collapseAllCws : i18n.ts.expandAllCws, - handler: () => { expandAllCws = !expandAllCws; }, + icon: `${expandAllCws.value ? 'ph-eye' : 'ph-eye-slash'} ph-bold ph-lg`, + text: expandAllCws.value ? i18n.ts.collapseAllCws : i18n.ts.expandAllCws, + handler: () => { expandAllCws.value = !expandAllCws.value; }, }, ] : []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(computed(() => note ? { +definePageMetadata(computed(() => note.value ? { title: i18n.ts.note, - subtitle: dateString(note.createdAt), - avatar: note.user, - path: `/notes/${note.id}`, + subtitle: dateString(note.value.createdAt), + avatar: note.value.user, + path: `/notes/${note.value.id}`, share: { - title: i18n.t('noteOf', { user: note.user.name }), - text: note.text, + title: i18n.t('noteOf', { user: note.value.user.name }), + text: note.value.text, }, } : null)); </script> diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index baccb4916c..f3fadf5c8e 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import XNotifications from '@/components/MkNotifications.vue'; import MkNotes from '@/components/MkNotes.vue'; import * as os from '@/os.js'; @@ -29,9 +29,9 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { notificationTypes } from '@/const.js'; -let tab = $ref('all'); -let includeTypes = $ref<string[] | null>(null); -const excludeTypes = $computed(() => includeTypes ? notificationTypes.filter(t => !includeTypes.includes(t)) : null); +const tab = ref('all'); +const includeTypes = ref<string[] | null>(null); +const excludeTypes = computed(() => includeTypes.value ? notificationTypes.filter(t => !includeTypes.value.includes(t)) : null); const mentionsPagination = { endpoint: 'notes/mentions' as const, @@ -49,27 +49,27 @@ const directNotesPagination = { function setFilter(ev) { const typeItems = notificationTypes.map(t => ({ text: i18n.t(`_notification._types.${t}`), - active: includeTypes && includeTypes.includes(t), + active: includeTypes.value && includeTypes.value.includes(t), action: () => { - includeTypes = [t]; + includeTypes.value = [t]; }, })); - const items = includeTypes != null ? [{ + const items = includeTypes.value != null ? [{ icon: 'ph-x ph-bold ph-lg', text: i18n.ts.clear, action: () => { - includeTypes = null; + includeTypes.value = null; }, - }, null, ...typeItems] : typeItems; + }, { type: 'divider' }, ...typeItems] : typeItems; os.popupMenu(items, ev.currentTarget ?? ev.target); } -const headerActions = $computed(() => [tab === 'all' ? { +const headerActions = computed(() => [tab.value === 'all' ? { text: i18n.ts.filter, icon: 'ph-funnel ph-bold ph-lg', - highlighted: includeTypes != null, + highlighted: includeTypes.value != null, handler: setFilter, -} : undefined, tab === 'all' ? { +} : undefined, tab.value === 'all' ? { text: i18n.ts.markAllAsRead, icon: 'ph-check ph-bold ph-lg', handler: () => { @@ -77,7 +77,7 @@ const headerActions = $computed(() => [tab === 'all' ? { }, } : undefined].filter(x => x !== undefined)); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'all', title: i18n.ts.all, icon: 'ph-circle ph-bold ph-lg', 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 2cb126c80f..f97c5ea1a7 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 @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> /* eslint-disable vue/no-mutating-props */ -import { onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; import XContainer from '../page-editor.container.vue'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os.js'; @@ -35,14 +35,14 @@ const emit = defineEmits<{ (ev: 'update:modelValue', value: any): void; }>(); -let file: any = $ref(null); +const file = ref<any>(null); async function choose() { os.selectDriveFile(false).then((fileResponse) => { - file = fileResponse[0]; + file.value = fileResponse[0]; emit('update:modelValue', { ...props.modelValue, - fileId: file.id, + fileId: file.value.id, }); }); } @@ -54,7 +54,7 @@ onMounted(async () => { os.api('drive/files/show', { fileId: props.modelValue.fileId, }).then(fileResponse => { - file = 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 0ed6c9bfa4..fc11ca8543 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 @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> /* eslint-disable vue/no-mutating-props */ -import { watch } from 'vue'; +import { watch, ref } from 'vue'; import XContainer from '../page-editor.container.vue'; import MkInput from '@/components/MkInput.vue'; import MkSwitch from '@/components/MkSwitch.vue'; @@ -40,19 +40,19 @@ const emit = defineEmits<{ (ev: 'update:modelValue', value: any): void; }>(); -let id: any = $ref(props.modelValue.note); -let note: any = $ref(null); +const id = ref<any>(props.modelValue.note); +const note = ref<any>(null); -watch($$(id), async () => { - if (id && (id.startsWith('http://') || id.startsWith('https://'))) { - id = (id.endsWith('/') ? id.slice(0, -1) : id).split('/').pop(); +watch(id, async () => { + if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) { + id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop(); } emit('update:modelValue', { ...props.modelValue, - note: id, + note: id.value, }); - note = await os.api('notes/show', { noteId: id }); + note.value = await os.api('notes/show', { noteId: id.value }); }, { immediate: true, }); diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue index f24131ebb7..885fa55bc9 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> /* eslint-disable vue/no-mutating-props */ -import { defineAsyncComponent, inject, onMounted, watch } from 'vue'; +import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue'; import { v4 as uuid } from 'uuid'; import XContainer from '../page-editor.container.vue'; import * as os from '@/os.js'; @@ -42,12 +42,12 @@ const emit = defineEmits<{ (ev: 'update:modelValue', value: any): void; }>(); -const children = $ref(deepClone(props.modelValue.children ?? [])); +const children = ref(deepClone(props.modelValue.children ?? [])); -watch($$(children), () => { +watch(children, () => { emit('update:modelValue', { ...props.modelValue, - children, + children: children.value, }); }, { deep: true, @@ -75,7 +75,7 @@ async function add() { if (canceled) return; const id = uuid(); - children.push({ id, type }); + children.value.push({ id, type }); } onMounted(() => { diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue index 8df74374b2..2af4e4e365 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue @@ -9,16 +9,17 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><i class="ph-text-align-left ph-bold ph-lg"></i> {{ i18n.ts._pages.blocks.text }}</template> <section> - <textarea v-model="text" :class="$style.textarea"></textarea> + <textarea ref="inputEl" v-model="text" :class="$style.textarea"></textarea> </section> </XContainer> </template> <script lang="ts" setup> /* eslint-disable vue/no-mutating-props */ -import { watch } from 'vue'; +import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue'; import XContainer from '../page-editor.container.vue'; import { i18n } from '@/i18n.js'; +import { Autocomplete } from '@/scripts/autocomplete.js'; const props = defineProps<{ modelValue: any @@ -28,14 +29,25 @@ const emit = defineEmits<{ (ev: 'update:modelValue', value: any): void; }>(); -const text = $ref(props.modelValue.text ?? ''); +let autocomplete: Autocomplete; -watch($$(text), () => { +const text = ref(props.modelValue.text ?? ''); +const inputEl = shallowRef<HTMLTextAreaElement | null>(null); + +watch(text, () => { emit('update:modelValue', { ...props.modelValue, - text, + text: text.value, }); }); + +onMounted(() => { + autocomplete = new Autocomplete(inputEl.value, text); +}); + +onUnmounted(() => { + autocomplete.detach(); +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/page-editor/page-editor.container.vue b/packages/frontend/src/pages/page-editor/page-editor.container.vue index 10effdda1e..71fa890f63 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.container.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue @@ -29,7 +29,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ expanded?: boolean; diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index f455f8904e..a6e68686df 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, provide, watch } from 'vue'; +import { computed, provide, watch, ref } from 'vue'; import { v4 as uuid } from 'uuid'; import XBlocks from './page-editor.blocks.vue'; import MkButton from '@/components/MkButton.vue'; @@ -81,47 +81,47 @@ const props = defineProps<{ initUser?: string; }>(); -let tab = $ref('settings'); -let author = $ref($i); -let readonly = $ref(false); -let page = $ref(null); -let pageId = $ref(null); -let currentName = $ref(null); -let title = $ref(''); -let summary = $ref(null); -let name = $ref(Date.now().toString()); -let eyeCatchingImage = $ref(null); -let eyeCatchingImageId = $ref(null); -let font = $ref('sans-serif'); -let content = $ref([]); -let alignCenter = $ref(false); -let hideTitleWhenPinned = $ref(false); +const tab = ref('settings'); +const author = ref($i); +const readonly = ref(false); +const page = ref(null); +const pageId = ref(null); +const currentName = ref(null); +const title = ref(''); +const summary = ref(null); +const name = ref(Date.now().toString()); +const eyeCatchingImage = ref(null); +const eyeCatchingImageId = ref(null); +const font = ref('sans-serif'); +const content = ref([]); +const alignCenter = ref(false); +const hideTitleWhenPinned = ref(false); -provide('readonly', readonly); +provide('readonly', readonly.value); provide('getPageBlockList', getPageBlockList); -watch($$(eyeCatchingImageId), async () => { - if (eyeCatchingImageId == null) { - eyeCatchingImage = null; +watch(eyeCatchingImageId, async () => { + if (eyeCatchingImageId.value == null) { + eyeCatchingImage.value = null; } else { - eyeCatchingImage = await os.api('drive/files/show', { - fileId: eyeCatchingImageId, + eyeCatchingImage.value = await os.api('drive/files/show', { + fileId: eyeCatchingImageId.value, }); } }); function getSaveOptions() { return { - title: title.trim(), - name: name.trim(), - summary: summary, - font: font, + title: title.value.trim(), + name: name.value.trim(), + summary: summary.value, + font: font.value, script: '', - hideTitleWhenPinned: hideTitleWhenPinned, - alignCenter: alignCenter, - content: content, + hideTitleWhenPinned: hideTitleWhenPinned.value, + alignCenter: alignCenter.value, + content: content.value, variables: [], - eyeCatchingImageId: eyeCatchingImageId, + eyeCatchingImageId: eyeCatchingImageId.value, }; } @@ -145,11 +145,11 @@ function save() { } }; - if (pageId) { - options.pageId = pageId; + if (pageId.value) { + options.pageId = pageId.value; os.api('pages/update', options) .then(page => { - currentName = name.trim(); + currentName.value = name.value.trim(); os.alert({ type: 'success', text: i18n.ts._pages.updated, @@ -158,13 +158,13 @@ function save() { } else { os.api('pages/create', options) .then(created => { - pageId = created.id; - currentName = name.trim(); + pageId.value = created.id; + currentName.value = name.value.trim(); os.alert({ type: 'success', text: i18n.ts._pages.created, }); - mainRouter.push(`/pages/edit/${pageId}`); + mainRouter.push(`/pages/edit/${pageId.value}`); }).catch(onError); } } @@ -172,11 +172,11 @@ function save() { function del() { os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: title.trim() }), + text: i18n.t('removeAreYouSure', { x: title.value.trim() }), }).then(({ canceled }) => { if (canceled) return; os.api('pages/delete', { - pageId: pageId, + pageId: pageId.value, }).then(() => { os.alert({ type: 'success', @@ -188,16 +188,16 @@ function del() { } function duplicate() { - title = title + ' - copy'; - name = name + '-copy'; + title.value = title.value + ' - copy'; + name.value = name.value + '-copy'; os.api('pages/create', getSaveOptions()).then(created => { - pageId = created.id; - currentName = name.trim(); + pageId.value = created.id; + currentName.value = name.value.trim(); os.alert({ type: 'success', text: i18n.ts._pages.created, }); - mainRouter.push(`/pages/edit/${pageId}`); + mainRouter.push(`/pages/edit/${pageId.value}`); }); } @@ -210,7 +210,7 @@ async function add() { if (canceled) return; const id = uuid(); - content.push({ id, type }); + content.value.push({ id, type }); } function getPageBlockList() { @@ -224,42 +224,42 @@ function getPageBlockList() { function setEyeCatchingImage(img) { selectFile(img.currentTarget ?? img.target, null).then(file => { - eyeCatchingImageId = file.id; + eyeCatchingImageId.value = file.id; }); } function removeEyeCatchingImage() { - eyeCatchingImageId = null; + eyeCatchingImageId.value = null; } async function init() { if (props.initPageId) { - page = await os.api('pages/show', { + page.value = await os.api('pages/show', { pageId: props.initPageId, }); } else if (props.initPageName && props.initUser) { - page = await os.api('pages/show', { + page.value = await os.api('pages/show', { name: props.initPageName, username: props.initUser, }); - readonly = true; + readonly.value = true; } - if (page) { - author = page.user; - pageId = page.id; - title = page.title; - name = page.name; - currentName = page.name; - summary = page.summary; - font = page.font; - hideTitleWhenPinned = page.hideTitleWhenPinned; - alignCenter = page.alignCenter; - content = page.content; - eyeCatchingImageId = page.eyeCatchingImageId; + if (page.value) { + author.value = page.value.user; + pageId.value = page.value.id; + title.value = page.value.title; + name.value = page.value.name; + currentName.value = page.value.name; + summary.value = page.value.summary; + font.value = page.value.font; + hideTitleWhenPinned.value = page.value.hideTitleWhenPinned; + alignCenter.value = page.value.alignCenter; + content.value = page.value.content; + eyeCatchingImageId.value = page.value.eyeCatchingImageId; } else { const id = uuid(); - content = [{ + content.value = [{ id, type: 'text', text: 'Hello World!', @@ -269,9 +269,9 @@ async function init() { init(); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'settings', title: i18n.ts._pages.pageSetting, icon: 'ph-gear ph-bold ph-lg', diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 962d910405..2cfb37ffa7 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -34,7 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="other"> <button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-rocket-launch ph-bold ph-lg ti-fw"></i></button> - <button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button> + <button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button> + <button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button> </div> </div> <div class="user"> @@ -75,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref } from 'vue'; import XPage from '@/components/page/page.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; @@ -90,30 +91,32 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { pageViewInterruptors, defaultStore } from '@/store.js'; import { deepClone } from '@/scripts/clone.js'; import { $i } from '@/account.js'; +import { isSupportShare } from '@/scripts/navigator.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; const props = defineProps<{ pageName: string; username: string; }>(); -let page = $ref(null); -let error = $ref(null); +const page = ref(null); +const error = ref(null); const otherPostsPagination = { endpoint: 'users/pages' as const, limit: 6, params: computed(() => ({ - userId: page.user.id, + userId: page.value.user.id, })), }; -const path = $computed(() => props.username + '/' + props.pageName); +const path = computed(() => props.username + '/' + props.pageName); function fetchPage() { - page = null; + page.value = null; os.api('pages/show', { name: props.pageName, username: props.username, }).then(async _page => { - page = _page; + page.value = _page; // plugin if (pageViewInterruptors.length > 0) { @@ -121,33 +124,38 @@ function fetchPage() { for (const interruptor of pageViewInterruptors) { result = await interruptor.handler(result); } - page = result; + page.value = result; } }).catch(err => { - error = err; + error.value = err; }); } function share() { navigator.share({ - title: page.title ?? page.name, - text: page.summary, - url: `${url}/@${page.user.username}/pages/${page.name}`, + title: page.value.title ?? page.value.name, + text: page.value.summary, + url: `${url}/@${page.value.user.username}/pages/${page.value.name}`, }); } +function copyLink() { + copyToClipboard(`${url}/@${page.value.user.username}/pages/${page.value.name}`); + os.success(); +} + function shareWithNote() { os.post({ - initialText: `${page.title || page.name} ${url}/@${page.user.username}/pages/${page.name}`, + initialText: `${page.value.title || page.value.name} ${url}/@${page.value.user.username}/pages/${page.value.name}`, }); } function like() { os.apiWithDialog('pages/like', { - pageId: page.id, + pageId: page.value.id, }).then(() => { - page.isLiked = true; - page.likedCount++; + page.value.isLiked = true; + page.value.likedCount++; }); } @@ -158,32 +166,32 @@ async function unlike() { }); if (confirm.canceled) return; os.apiWithDialog('pages/unlike', { - pageId: page.id, + pageId: page.value.id, }).then(() => { - page.isLiked = false; - page.likedCount--; + page.value.isLiked = false; + page.value.likedCount--; }); } function pin(pin) { os.apiWithDialog('i/update', { - pinnedPageId: pin ? page.id : null, + pinnedPageId: pin ? page.value.id : null, }); } -watch(() => path, fetchPage, { immediate: true }); +watch(() => path.value, fetchPage, { immediate: true }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(computed(() => page ? { - title: page.title || page.name, - avatar: page.user, - path: `/@${page.user.username}/pages/${page.name}`, +definePageMetadata(computed(() => page.value ? { + title: page.value.title || page.value.name, + avatar: page.value.user, + path: `/@${page.value.user.username}/pages/${page.value.name}`, share: { - title: page.title || page.name, - text: page.summary, + title: page.value.title || page.value.name, + text: page.value.summary, }, } : null)); </script> diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue index b699f77b85..a7ca433ed3 100644 --- a/packages/frontend/src/pages/pages.vue +++ b/packages/frontend/src/pages/pages.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import MkPagePreview from '@/components/MkPagePreview.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; @@ -46,7 +46,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; const router = useRouter(); -let tab = $ref('featured'); +const tab = ref('featured'); const featuredPagesPagination = { endpoint: 'pages/featured' as const, @@ -65,13 +65,13 @@ function create() { router.push('/pages/new'); } -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.create, handler: create, }]); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'featured', title: i18n.ts._pages.featured, icon: 'ph-fire ph-bold ph-lg', diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index c762110ead..7e4a0b508d 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, computed, ref } from 'vue'; import JSON5 from 'json5'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; @@ -49,16 +49,16 @@ const props = defineProps<{ domain: string; }>(); -const scope = $computed(() => props.path ? props.path.split('/') : []); +const scope = computed(() => props.path ? props.path.split('/') : []); -let keys = $ref(null); +const keys = ref(null); function fetchKeys() { os.api('i/registry/keys-with-type', { - scope: scope, + scope: scope.value, domain: props.domain === '@' ? null : props.domain, }).then(res => { - keys = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0])); + keys.value = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0])); }); } @@ -76,7 +76,7 @@ async function createKey() { scope: { type: 'string', label: i18n.ts._registry.scope, - default: scope.join('/'), + default: scope.value.join('/'), }, }); if (canceled) return; @@ -91,9 +91,9 @@ async function createKey() { watch(() => props.path, fetchKeys, { immediate: true }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.registry, diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue index 7206ac7b46..baa88ec008 100644 --- a/packages/frontend/src/pages/registry.value.vue +++ b/packages/frontend/src/pages/registry.value.vue @@ -26,9 +26,9 @@ SPDX-License-Identifier: AGPL-3.0-only </MkKeyValue> </FormSplit> - <MkTextarea v-model="valueForEditor" tall class="_monospace"> + <MkCodeEditor v-model="valueForEditor" lang="json5"> <template #label>{{ i18n.ts.value }} (JSON)</template> - </MkTextarea> + </MkCodeEditor> <MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> @@ -45,14 +45,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, computed, ref } from 'vue'; import JSON5 from 'json5'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; -import MkTextarea from '@/components/MkTextarea.vue'; +import MkCodeEditor from '@/components/MkCodeEditor.vue'; import FormSplit from '@/components/form/split.vue'; import FormInfo from '@/components/MkInfo.vue'; @@ -61,26 +61,26 @@ const props = defineProps<{ domain: string; }>(); -const scope = $computed(() => props.path.split('/').slice(0, -1)); -const key = $computed(() => props.path.split('/').at(-1)); +const scope = computed(() => props.path.split('/').slice(0, -1)); +const key = computed(() => props.path.split('/').at(-1)); -let value = $ref(null); -let valueForEditor = $ref(null); +const value = ref(null); +const valueForEditor = ref(null); function fetchValue() { os.api('i/registry/get-detail', { - scope, - key, + scope: scope.value, + key: key.value, domain: props.domain === '@' ? null : props.domain, }).then(res => { - value = res; - valueForEditor = JSON5.stringify(res.value, null, '\t'); + value.value = res; + valueForEditor.value = JSON5.stringify(res.value, null, '\t'); }); } async function save() { try { - JSON5.parse(valueForEditor); + JSON5.parse(valueForEditor.value); } catch (err) { os.alert({ type: 'error', @@ -94,9 +94,9 @@ async function save() { }).then(({ canceled }) => { if (canceled) return; os.apiWithDialog('i/registry/set', { - scope, - key, - value: JSON5.parse(valueForEditor), + scope: scope.value, + key: key.value, + value: JSON5.parse(valueForEditor.value), domain: props.domain === '@' ? null : props.domain, }); }); @@ -109,8 +109,8 @@ function del() { }).then(({ canceled }) => { if (canceled) return; os.apiWithDialog('i/registry/remove', { - scope, - key, + scope: scope.value, + key: key.value, domain: props.domain === '@' ? null : props.domain, }); }); @@ -118,9 +118,9 @@ function del() { watch(() => props.path, fetchValue, { immediate: true }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.registry, diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue index e462a21f9a..0572102849 100644 --- a/packages/frontend/src/pages/registry.vue +++ b/packages/frontend/src/pages/registry.vue @@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref, computed } from 'vue'; import JSON5 from 'json5'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; @@ -30,11 +31,11 @@ import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/MkButton.vue'; -let scopesWithDomain = $ref(null); +const scopesWithDomain = ref(null); function fetchScopes() { os.api('i/registry/scopes-with-domain').then(res => { - scopesWithDomain = res; + scopesWithDomain.value = res; }); } @@ -66,9 +67,9 @@ async function createKey() { fetchScopes(); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.registry, diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue index c1fdfdbeb6..1aed57724e 100644 --- a/packages/frontend/src/pages/reset-password.vue +++ b/packages/frontend/src/pages/reset-password.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, onMounted } from 'vue'; +import { defineAsyncComponent, onMounted, ref, computed } from 'vue'; import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; @@ -32,12 +32,12 @@ const props = defineProps<{ token?: string; }>(); -let password = $ref(''); +const password = ref(''); async function save() { await os.apiWithDialog('reset-password', { token: props.token, - password: password, + password: password.value, }); mainRouter.push('/'); } @@ -49,9 +49,9 @@ onMounted(() => { } }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.resetPassword, diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index 193505f628..c99f66f012 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref } from 'vue'; import * as os from '@/os.js'; import MkUserList from '@/components/MkUserList.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -52,29 +52,29 @@ const props = withDefaults(defineProps<{ initialTab: 'users', }); -let tab = $ref(props.initialTab); -let role = $ref(); -let error = $ref(); -let visible = $ref(false); +const tab = ref(props.initialTab); +const role = ref(); +const error = ref(); +const visible = ref(false); watch(() => props.role, () => { os.api('roles/show', { roleId: props.role, }).then(res => { - role = res; - document.title = `${role?.name} | ${instanceName}`; - visible = res.isExplorable && res.isPublic; + role.value = res; + document.title = `${role.value?.name} | ${instanceName}`; + visible.value = res.isExplorable && res.isPublic; }).catch((err) => { if (err.code === 'NO_SUCH_ROLE') { - error = i18n.ts.noRole; + error.value = i18n.ts.noRole; } else { - error = i18n.ts.somethingHappened; + error.value = i18n.ts.somethingHappened; } - document.title = `${error} | ${instanceName}`; + document.title = `${error.value} | ${instanceName}`; }); }, { immediate: true }); -const users = $computed(() => ({ +const users = computed(() => ({ endpoint: 'roles/users' as const, limit: 30, params: { @@ -82,7 +82,7 @@ const users = $computed(() => ({ }, })); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'users', icon: 'ph-users ph-bold ph-lg', title: i18n.ts.users, @@ -93,7 +93,7 @@ const headerTabs = $computed(() => [{ }]); definePageMetadata(computed(() => ({ - title: role?.name, + title: role.value?.name, icon: 'ph-seal-check ph-bold ph-lg', }))); </script> diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue index 6800481d55..ccda5bb8ac 100644 --- a/packages/frontend/src/pages/scratchpad.vue +++ b/packages/frontend/src/pages/scratchpad.vue @@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onDeactivated, onUnmounted, Ref, ref, watch } from 'vue'; +import { onDeactivated, onUnmounted, Ref, ref, watch, computed } from 'vue'; import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import MkContainer from '@/components/MkContainer.vue'; import MkButton from '@/components/MkButton.vue'; @@ -59,8 +59,8 @@ let aiscript: Interpreter; const code = ref(''); const logs = ref<any[]>([]); const root = ref<AsUiRoot>(); -let components: Ref<AsUiComponent>[] = $ref([]); -let uiKey = $ref(0); +const components = ref<Ref<AsUiComponent>[]>([]); +const uiKey = ref(0); const saved = miLocalStorage.getItem('scratchpad'); if (saved) { @@ -74,15 +74,15 @@ watch(code, () => { async function run() { if (aiscript) aiscript.abort(); root.value = undefined; - components = []; - uiKey++; + components.value = []; + uiKey.value++; logs.value = []; aiscript = new Interpreter(({ ...createAiScriptEnv({ storageKey: 'widget', token: $i?.token, }), - ...registerAsUiLib(components, (_root) => { + ...registerAsUiLib(components.value, (_root) => { root.value = _root.value; }), }), { @@ -160,9 +160,9 @@ onUnmounted(() => { if (aiscript) aiscript.abort(); }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.scratchpad, diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index 0b1740847d..acfa5b9fdf 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -50,41 +50,37 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted } from 'vue'; +import { ref } from 'vue'; import MkNotes from '@/components/MkNotes.vue'; import MkInput from '@/components/MkInput.vue'; -import MkRadios from '@/components/MkRadios.vue'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; -import { $i } from '@/account.js'; -import { instance } from '@/instance.js'; -import MkInfo from '@/components/MkInfo.vue'; import { useRouter } from '@/router.js'; import MkFolder from '@/components/MkFolder.vue'; const router = useRouter(); -let key = $ref(0); -let searchQuery = $ref(''); -let searchOrigin = $ref('combined'); -let notePagination = $ref(); -let user = $ref(null); -let isLocalOnly = $ref(false); -let order = $ref(true); -let filetype = $ref(null); +const key = ref(0); +const searchQuery = ref(''); +const searchOrigin = ref('combined'); +const notePagination = ref(); +const user = ref(null); +const isLocalOnly = ref(false); +const order = ref(false); +const filetype = ref(null); function selectUser() { os.selectUser().then(_user => { - user = _user; + user.value = _user; }); } async function search() { - const query = searchQuery.toString().trim(); + const query = searchQuery.value.toString().trim(); if (query == null || query === '') return; @@ -106,19 +102,19 @@ async function search() { return; } - notePagination = { + notePagination.value = { endpoint: 'notes/search', limit: 10, params: { - query: searchQuery, - userId: user ? user.id : null, - order: order ? 'desc' : 'asc', - filetype: filetype, + query: searchQuery.value, + userId: user.value ? user.value.id : null, + order: order.value ? 'desc' : 'asc', + filetype: filetype.value, }, }; - if (isLocalOnly) notePagination.params.host = '.'; + if (isLocalOnly.value) notePagination.value.params.host = '.'; - key++; + key.value++; } </script> diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index c0b2e55986..0485012fdb 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, onMounted } from 'vue'; +import { ref } from 'vue'; import MkUserList from '@/components/MkUserList.vue'; import MkInput from '@/components/MkInput.vue'; import MkRadios from '@/components/MkRadios.vue'; @@ -33,20 +33,17 @@ import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; -import { $i } from '@/account.js'; -import { instance } from '@/instance.js'; -import MkInfo from '@/components/MkInfo.vue'; import { useRouter } from '@/router.js'; const router = useRouter(); -let key = $ref(''); -let searchQuery = $ref(''); -let searchOrigin = $ref('combined'); -let userPagination = $ref(); +const key = ref(''); +const searchQuery = ref(''); +const searchOrigin = ref('combined'); +const userPagination = ref(); async function search() { - const query = searchQuery.toString().trim(); + const query = searchQuery.value.toString().trim(); if (query == null || query === '') return; @@ -68,15 +65,15 @@ async function search() { return; } - userPagination = { + userPagination.value = { endpoint: 'users/search', limit: 10, params: { query: query, - origin: searchOrigin, + origin: searchOrigin.value, }, }; - key = query; + key.value = query; } </script> diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue index eae120cb34..acc291c73e 100644 --- a/packages/frontend/src/pages/search.vue +++ b/packages/frontend/src/pages/search.vue @@ -23,10 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, onMounted } from 'vue'; +import { computed, defineAsyncComponent, ref } from 'vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import * as os from '@/os.js'; import { $i } from '@/account.js'; import { instance } from '@/instance.js'; import MkInfo from '@/components/MkInfo.vue'; @@ -34,13 +33,13 @@ import MkInfo from '@/components/MkInfo.vue'; const XNote = defineAsyncComponent(() => import('./search.note.vue')); const XUser = defineAsyncComponent(() => import('./search.user.vue')); -let tab = $ref('note'); +const tab = ref('note'); const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes)); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => [{ +const headerTabs = computed(() => [{ key: 'note', title: i18n.ts.notes, icon: 'ph-pencil ph-bold ph-lg', diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index 4e2f367374..09421ba2c2 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, defineAsyncComponent } from 'vue'; +import { defineAsyncComponent, computed } from 'vue'; import { supported as webAuthnSupported, create as webAuthnCreate, parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -91,7 +91,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 5bce040cef..642100342e 100644 --- a/packages/frontend/src/pages/settings/accounts.vue +++ b/packages/frontend/src/pages/settings/accounts.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, ref } from 'vue'; +import { defineAsyncComponent, ref, computed } from 'vue'; import type * as Misskey from 'misskey-js'; import FormSuspense from '@/components/form/suspense.vue'; import MkButton from '@/components/MkButton.vue'; @@ -101,9 +101,9 @@ function switchAccountWithToken(token: string) { login(token); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.accounts, diff --git a/packages/frontend/src/pages/settings/api.vue b/packages/frontend/src/pages/settings/api.vue index 2f03d07c6e..ca38bd2e3d 100644 --- a/packages/frontend/src/pages/settings/api.vue +++ b/packages/frontend/src/pages/settings/api.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, ref } from 'vue'; +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'; @@ -40,9 +40,9 @@ function generateToken() { }, 'closed'); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: 'API', diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue index db8dcd82f5..424d9fd4c9 100644 --- a/packages/frontend/src/pages/settings/apps.vue +++ b/packages/frontend/src/pages/settings/apps.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { ref, computed } from 'vue'; import FormPagination from '@/components/MkPagination.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; @@ -71,9 +71,9 @@ function revoke(token) { }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.installedApps, diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue new file mode 100644 index 0000000000..9c95b5547e --- /dev/null +++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue @@ -0,0 +1,69 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + :class="[$style.root, { [$style.active]: active }]" + @click="emit('click')" +> + <div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div> + <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY }]" forceShowDecoration/> + <i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i> +</div> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import { $i } from '@/account.js'; + +const props = defineProps<{ + active?: boolean; + decoration: { + id: string; + url: string; + name: string; + roleIdsThatCanBeUsedThisDecoration: string[]; + }; + angle?: number; + flipH?: boolean; + offsetX?: number; + offsetY?: number; +}>(); + +const emit = defineEmits<{ + (ev: 'click'): void; +}>(); +</script> + +<style lang="scss" module> +.root { + cursor: pointer; + padding: 16px 16px 28px 16px; + border: solid 2px var(--divider); + border-radius: 8px; + text-align: center; + font-size: 90%; + overflow: clip; + contain: content; +} + +.active { + background-color: var(--accentedBg); + border-color: var(--accent); +} + +.name { + position: relative; + z-index: 10; + font-weight: bold; + margin-bottom: 20px; +} + +.lock { + position: absolute; + bottom: 12px; + right: 12px; +} +</style> diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue new file mode 100644 index 0000000000..329ab4d47a --- /dev/null +++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue @@ -0,0 +1,154 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialog" + :width="400" + :height="450" + @close="cancel" + @closed="emit('closed')" +> + <template #header>{{ i18n.ts.avatarDecorations }}</template> + + <div> + <MkSpacer :marginMin="20" :marginMax="28"> + <div style="text-align: center;"> + <div :class="$style.name">{{ decoration.name }}</div> + <MkAvatar style="width: 64px; height: 64px; margin-bottom: 20px;" :user="$i" :decorations="decorationsForPreview" forceShowDecoration/> + </div> + <div class="_gaps_s"> + <MkRange v-model="angle" continuousUpdate :min="-0.5" :max="0.5" :step="0.025" :textConverter="(v) => `${Math.floor(v * 360)}°`"> + <template #label>{{ i18n.ts.angle }}</template> + </MkRange> + <MkRange v-model="offsetX" continuousUpdate :min="-0.25" :max="0.25" :step="0.025" :textConverter="(v) => `${Math.floor(v * 100)}%`"> + <template #label>X {{ i18n.ts.position }}</template> + </MkRange> + <MkRange v-model="offsetY" continuousUpdate :min="-0.25" :max="0.25" :step="0.025" :textConverter="(v) => `${Math.floor(v * 100)}%`"> + <template #label>Y {{ i18n.ts.position }}</template> + </MkRange> + <MkSwitch v-model="flipH"> + <template #label>{{ i18n.ts.flip }}</template> + </MkSwitch> + </div> + </MkSpacer> + + <div :class="$style.footer" class="_buttonsCenter"> + <MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton> + <MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton> + <MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton> + </div> + </div> +</MkModalWindow> +</template> + +<script lang="ts" setup> +import { shallowRef, ref, computed } from 'vue'; +import MkButton from '@/components/MkButton.vue'; +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'; + +const props = defineProps<{ + usingIndex: number | null; + decoration: { + id: string; + url: string; + name: string; + }; +}>(); + +const emit = defineEmits<{ + (ev: 'closed'): void; + (ev: 'attach', payload: { + angle: number; + flipH: boolean; + offsetX: number; + offsetY: number; + }): void; + (ev: 'update', payload: { + angle: number; + flipH: boolean; + offsetX: number; + offsetY: number; + }): void; + (ev: 'detach'): void; +}>(); + +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0); +const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0); +const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false); +const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0); +const offsetY = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetY : null) ?? 0); + +const decorationsForPreview = computed(() => { + const decoration = { + id: props.decoration.id, + url: props.decoration.url, + angle: angle.value, + flipH: flipH.value, + offsetX: offsetX.value, + offsetY: offsetY.value, + }; + const decorations = [...$i.avatarDecorations]; + if (props.usingIndex != null) { + decorations[props.usingIndex] = decoration; + } else { + decorations.push(decoration); + } + return decorations; +}); + +function cancel() { + dialog.value.close(); +} + +async function update() { + emit('update', { + angle: angle.value, + flipH: flipH.value, + offsetX: offsetX.value, + offsetY: offsetY.value, + }); + dialog.value.close(); +} + +async function attach() { + emit('attach', { + angle: angle.value, + flipH: flipH.value, + offsetX: offsetX.value, + offsetY: offsetY.value, + }); + dialog.value.close(); +} + +async function detach() { + emit('detach'); + dialog.value.close(); +} +</script> + +<style lang="scss" module> +.name { + position: relative; + z-index: 10; + font-weight: bold; + margin-bottom: 28px; +} + +.footer { + position: sticky; + bottom: 0; + left: 0; + padding: 12px; + border-top: solid 0.5px var(--divider); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} +</style> diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue new file mode 100644 index 0000000000..6551fc917e --- /dev/null +++ b/packages/frontend/src/pages/settings/avatar-decoration.vue @@ -0,0 +1,152 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <div v-if="!loading" class="_gaps"> + <MkInfo>{{ i18n.t('_profile.avatarDecorationMax', { max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.t('remainingN', { n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo> + + <MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration/> + + <div v-if="$i.avatarDecorations.length > 0" v-panel :class="$style.current" class="_gaps_s"> + <div>{{ i18n.ts.inUse }}</div> + + <div :class="$style.decorations"> + <XDecoration + v-for="(avatarDecoration, i) in $i.avatarDecorations" + :decoration="avatarDecorations.find(d => d.id === avatarDecoration.id)" + :angle="avatarDecoration.angle" + :flipH="avatarDecoration.flipH" + :offsetX="avatarDecoration.offsetX" + :offsetY="avatarDecoration.offsetY" + :active="true" + @click="openDecoration(avatarDecoration, i)" + /> + </div> + + <MkButton danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton> + </div> + + <div :class="$style.decorations"> + <XDecoration + v-for="avatarDecoration in avatarDecorations" + :key="avatarDecoration.id" + :decoration="avatarDecoration" + @click="openDecoration(avatarDecoration)" + /> + </div> + </div> + <div v-else> + <MkLoading/> + </div> +</div> +</template> + +<script lang="ts" setup> +import { ref, defineAsyncComponent, computed } from 'vue'; +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 { i18n } from '@/i18n.js'; +import { $i } from '@/account.js'; +import MkInfo from '@/components/MkInfo.vue'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; + +const loading = ref(true); +const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse>([]); + +os.api('get-avatar-decorations').then(_avatarDecorations => { + avatarDecorations.value = _avatarDecorations; + loading.value = false; +}); + +function openDecoration(avatarDecoration, index?: number) { + os.popup(defineAsyncComponent(() => import('./avatar-decoration.dialog.vue')), { + decoration: avatarDecoration, + usingIndex: index, + }, { + 'attach': async (payload) => { + const decoration = { + id: avatarDecoration.id, + angle: payload.angle, + flipH: payload.flipH, + offsetX: payload.offsetX, + offsetY: payload.offsetY, + }; + const update = [...$i.avatarDecorations, decoration]; + await os.apiWithDialog('i/update', { + avatarDecorations: update, + }); + $i.avatarDecorations = update; + }, + 'update': async (payload) => { + const decoration = { + id: avatarDecoration.id, + angle: payload.angle, + flipH: payload.flipH, + offsetX: payload.offsetX, + offsetY: payload.offsetY, + }; + const update = [...$i.avatarDecorations]; + update[index] = decoration; + await os.apiWithDialog('i/update', { + avatarDecorations: update, + }); + $i.avatarDecorations = update; + }, + 'detach': async () => { + const update = [...$i.avatarDecorations]; + update.splice(index, 1); + await os.apiWithDialog('i/update', { + avatarDecorations: update, + }); + $i.avatarDecorations = update; + }, + }, 'closed'); +} + +function detachAllDecorations() { + os.confirm({ + type: 'warning', + text: i18n.ts.areYouSure, + }).then(async ({ canceled }) => { + if (canceled) return; + await os.apiWithDialog('i/update', { + avatarDecorations: [], + }); + $i.avatarDecorations = []; + }); +} + +const headerActions = computed(() => []); + +const headerTabs = computed(() => []); + +definePageMetadata({ + title: i18n.ts.avatarDecorations, + icon: 'ti ti-sparkles', +}); +</script> + +<style lang="scss" module> +.avatar { + display: inline-block; + width: 72px; + height: 72px; + margin: 16px auto; +} + +.current { + padding: 16px; + border-radius: var(--radius); +} + +.decorations { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + grid-gap: 12px; +} +</style> diff --git a/packages/frontend/src/pages/settings/custom-css.vue b/packages/frontend/src/pages/settings/custom-css.vue index 6e1481ae97..00a1fca856 100644 --- a/packages/frontend/src/pages/settings/custom-css.vue +++ b/packages/frontend/src/pages/settings/custom-css.vue @@ -7,15 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <FormInfo warn>{{ i18n.ts.customCssWarn }}</FormInfo> - <MkTextarea v-model="localCustomCss" manualSave tall class="_monospace" style="tab-size: 2;"> + <MkCodeEditor v-model="localCustomCss" manualSave lang="css"> <template #label>CSS</template> - </MkTextarea> + </MkCodeEditor> </div> </template> <script lang="ts" setup> -import { ref, watch } from 'vue'; -import MkTextarea from '@/components/MkTextarea.vue'; +import { ref, watch, computed } from 'vue'; +import MkCodeEditor from '@/components/MkCodeEditor.vue'; import FormInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; import { unisonReload } from '@/scripts/unison-reload.js'; @@ -41,9 +41,9 @@ watch(localCustomCss, async () => { await apply(); }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.customCss, diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue index ff9a78cd52..32acd5e7a6 100644 --- a/packages/frontend/src/pages/settings/deck.vue +++ b/packages/frontend/src/pages/settings/deck.vue @@ -32,9 +32,9 @@ const useSimpleUiForNonRootPages = computed(deckStore.makeGetterSetter('useSimpl const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn')); const columnAlign = computed(deckStore.makeGetterSetter('columnAlign')); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.deck, diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue index 00dc022259..601479b73c 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -55,12 +55,11 @@ import MkPagination from '@/components/MkPagination.vue'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import { i18n } from '@/i18n.js'; import bytes from '@/filters/bytes.js'; -import { dateString } from '@/filters/date.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkSelect from '@/components/MkSelect.vue'; import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js'; -let sortMode = ref('+size'); +const sortMode = ref('+size'); const pagination = { endpoint: 'drive/files' as const, limit: 10, diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index 5b9bcce944..fc2ce45bf0 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -72,7 +72,7 @@ const fetching = ref(true); const usage = ref<any>(null); const capacity = ref<any>(null); const uploadFolder = ref<any>(null); -let alwaysMarkNsfw = $ref($i.alwaysMarkNsfw); +const alwaysMarkNsfw = ref($i.alwaysMarkNsfw); const meterStyle = computed(() => { return { @@ -117,20 +117,20 @@ function chooseUploadFolder() { function saveProfile() { os.api('i/update', { - alwaysMarkNsfw: !!alwaysMarkNsfw, + alwaysMarkNsfw: !!alwaysMarkNsfw.value, }).catch(err => { os.alert({ type: 'error', title: i18n.ts.error, text: err.message, }); - alwaysMarkNsfw = true; + alwaysMarkNsfw.value = true; }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.drive, diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue index 6e7d78292f..003501f45a 100644 --- a/packages/frontend/src/pages/settings/email.vue +++ b/packages/frontend/src/pages/settings/email.vue @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, ref, watch } from 'vue'; +import { onMounted, ref, watch, computed } from 'vue'; import FormSection from '@/components/form/section.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkInput from '@/components/MkInput.vue'; @@ -106,9 +106,9 @@ onMounted(() => { }); }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.email, diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue new file mode 100644 index 0000000000..61f3332122 --- /dev/null +++ b/packages/frontend/src/pages/settings/emoji-picker.vue @@ -0,0 +1,274 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="_gaps_m"> + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-pin"></i></template> + <template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.reaction }})</template> + <template #caption>{{ i18n.ts.pinnedEmojisForReactionSettingDescription }}</template> + + <div class="_gaps"> + <div> + <div v-panel style="border-radius: 6px;"> + <Sortable + v-model="pinnedEmojisForReaction" + :class="$style.emojis" + :itemKey="item => item" + :animation="150" + :delay="100" + :delayOnTouchOnly="true" + > + <template #item="{element}"> + <button class="_button" :class="$style.emojisItem" @click="removeReaction(element, $event)"> + <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/> + <MkEmoji v-else :emoji="element" :normal="true"/> + </button> + </template> + <template #footer> + <button class="_button" :class="$style.emojisAdd" @click="chooseReaction"> + <i class="ti ti-plus"></i> + </button> + </template> + </Sortable> + </div> + <div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div> + </div> + + <div class="_buttons"> + <MkButton inline @click="previewReaction"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton> + <MkButton inline danger @click="setDefaultReaction"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> + <MkButton inline danger @click="overwriteFromPinnedEmojis"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojis }}</MkButton> + </div> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-pin"></i></template> + <template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.general }})</template> + <template #caption>{{ i18n.ts.pinnedEmojisSettingDescription }}</template> + + <div class="_gaps"> + <div> + <div v-panel style="border-radius: 6px;"> + <Sortable + v-model="pinnedEmojis" + :class="$style.emojis" + :itemKey="item => item" + :animation="150" + :delay="100" + :delayOnTouchOnly="true" + > + <template #item="{element}"> + <button class="_button" :class="$style.emojisItem" @click="removeEmoji(element, $event)"> + <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/> + <MkEmoji v-else :emoji="element" :normal="true"/> + </button> + </template> + <template #footer> + <button class="_button" :class="$style.emojisAdd" @click="chooseEmoji"> + <i class="ti ti-plus"></i> + </button> + </template> + </Sortable> + </div> + <div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div> + </div> + + <div class="_buttons"> + <MkButton inline @click="previewEmoji"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton> + <MkButton inline danger @click="setDefaultEmoji"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> + <MkButton inline danger @click="overwriteFromPinnedEmojisForReaction"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojisForReaction }}</MkButton> + </div> + </div> + </MkFolder> + + <FormSection> + <template #label>{{ i18n.ts.emojiPickerDisplay }}</template> + + <div class="_gaps_m"> + <MkRadios v-model="emojiPickerScale"> + <template #label>{{ i18n.ts.size }}</template> + <option :value="1">{{ i18n.ts.small }}</option> + <option :value="2">{{ i18n.ts.medium }}</option> + <option :value="3">{{ i18n.ts.large }}</option> + </MkRadios> + + <MkRadios v-model="emojiPickerWidth"> + <template #label>{{ i18n.ts.numberOfColumn }}</template> + <option :value="1">5</option> + <option :value="2">6</option> + <option :value="3">7</option> + <option :value="4">8</option> + <option :value="5">9</option> + </MkRadios> + + <MkRadios v-model="emojiPickerHeight"> + <template #label>{{ i18n.ts.height }}</template> + <option :value="1">{{ i18n.ts.small }}</option> + <option :value="2">{{ i18n.ts.medium }}</option> + <option :value="3">{{ i18n.ts.large }}</option> + <option :value="4">{{ i18n.ts.large }}+</option> + </MkRadios> + + <MkSwitch v-model="emojiPickerUseDrawerForMobile"> + {{ i18n.ts.useDrawerReactionPickerForMobile }} + <template #caption>{{ i18n.ts.needReloadToApply }}</template> + </MkSwitch> + </div> + </FormSection> +</div> +</template> + +<script lang="ts" setup> +import { computed, ref, Ref, watch } from 'vue'; +import Sortable from 'vuedraggable'; +import MkRadios from '@/components/MkRadios.vue'; +import MkButton from '@/components/MkButton.vue'; +import FormSection from '@/components/form/section.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import * as os from '@/os.js'; +import { defaultStore } from '@/store.js'; +import { i18n } from '@/i18n.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { deepClone } from '@/scripts/clone.js'; +import { reactionPicker } from '@/scripts/reaction-picker.js'; +import { emojiPicker } from '@/scripts/emoji-picker.js'; +import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; +import MkEmoji from '@/components/global/MkEmoji.vue'; +import MkFolder from '@/components/MkFolder.vue'; + +const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(defaultStore.state.reactions)); +const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmojis)); + +const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale')); +const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth')); +const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight')); +const emojiPickerUseDrawerForMobile = computed(defaultStore.makeGetterSetter('emojiPickerUseDrawerForMobile')); + +const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev); +const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev); +const setDefaultReaction = () => setDefault(pinnedEmojisForReaction); + +const removeEmoji = (reaction: string, ev: MouseEvent) => remove(pinnedEmojis, reaction, ev); +const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev); +const setDefaultEmoji = () => setDefault(pinnedEmojis); + +function previewReaction(ev: MouseEvent) { + reactionPicker.show(getHTMLElement(ev)); +} + +function previewEmoji(ev: MouseEvent) { + emojiPicker.show(getHTMLElement(ev)); +} + +async function overwriteFromPinnedEmojis() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.overwriteContentConfirm, + }); + + if (canceled) { + return; + } + + pinnedEmojisForReaction.value = [...pinnedEmojis.value]; +} + +async function overwriteFromPinnedEmojisForReaction() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.overwriteContentConfirm, + }); + + if (canceled) { + return; + } + + pinnedEmojis.value = [...pinnedEmojisForReaction.value]; +} + +function remove(itemsRef: Ref<string[]>, reaction: string, ev: MouseEvent) { + os.popupMenu([{ + text: i18n.ts.remove, + action: () => { + itemsRef.value = itemsRef.value.filter(x => x !== reaction); + }, + }], getHTMLElement(ev)); +} + +async function setDefault(itemsRef: Ref<string[]>) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.resetAreYouSure, + }); + if (canceled) return; + + itemsRef.value = deepClone(defaultStore.def.reactions.default); +} + +async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) { + os.pickEmoji(getHTMLElement(ev), { + showPinned: false, + }).then(it => { + const emoji = it as string; + if (!itemsRef.value.includes(emoji)) { + itemsRef.value.push(emoji); + } + }); +} + +function getHTMLElement(ev: MouseEvent): HTMLElement { + const target = ev.currentTarget ?? ev.target; + return target as HTMLElement; +} + +watch(pinnedEmojisForReaction, () => { + defaultStore.set('reactions', pinnedEmojisForReaction.value); +}, { + deep: true, +}); + +watch(pinnedEmojis, () => { + defaultStore.set('pinnedEmojis', pinnedEmojis.value); +}, { + deep: true, +}); + +definePageMetadata({ + title: i18n.ts.emojiPicker, + icon: 'ti ti-mood-happy', +}); +</script> + +<style lang="scss" module> +.tab { + margin: calc(var(--margin) / 2) 0; + padding: calc(var(--margin) / 2) 0; + background: var(--bg); +} + +.emojis { + padding: 12px; + font-size: 1.1em; +} + +.emojisItem { + display: inline-block; + padding: 8px; + cursor: move; +} + +.emojisAdd { + display: inline-block; + padding: 8px; +} + +.editorCaption { + font-size: 0.85em; + padding: 8px 0 0 0; + color: var(--fgTransparentWeak); +} +</style> diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 32dda149b8..0839a65ebb 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -66,6 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="sharkey"><i class="sk-icons sk-shark ph-bold" style="top: 2px;position: relative;"></i> Sharkey</option> <option value="misskey"><i class="sk-icons sk-misskey ph-bold" style="top: 2px;position: relative;"></i> Misskey</option> </MkRadios> + <MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch> </div> <MkSelect v-model="instanceTicker"> @@ -136,7 +137,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch> <MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch> <MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch> - <MkSwitch v-model="enableDataSaverMode">{{ i18n.ts.dataSaver }}</MkSwitch> + <MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch> </div> <div> <MkRadios v-model="emojiStyle"> @@ -187,6 +188,37 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.numberOfPageCache }}</template> <template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template> </MkRange> + + <MkFolder> + <template #label>{{ i18n.ts.dataSaver }}</template> + + <div class="_gaps_m"> + <MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo> + + <div class="_buttons"> + <MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton> + <MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton> + </div> + <div class="_gaps_m"> + <MkSwitch v-model="dataSaver.media"> + {{ i18n.ts._dataSaver._media.title }} + <template #caption>{{ i18n.ts._dataSaver._media.description }}</template> + </MkSwitch> + <MkSwitch v-model="dataSaver.avatar"> + {{ i18n.ts._dataSaver._avatar.title }} + <template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template> + </MkSwitch> + <MkSwitch v-model="dataSaver.urlPreview"> + {{ i18n.ts._dataSaver._urlPreview.title }} + <template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template> + </MkSwitch> + <MkSwitch v-model="dataSaver.code"> + {{ i18n.ts._dataSaver._code.title }} + <template #caption>{{ i18n.ts._dataSaver._code.description }}</template> + </MkSwitch> + </div> + </div> + </MkFolder> </div> </FormSection> @@ -220,6 +252,7 @@ import MkButton from '@/components/MkButton.vue'; import FormSection from '@/components/form/section.vue'; import FormLink from '@/components/form/link.vue'; import MkLink from '@/components/MkLink.vue'; +import MkInfo from '@/components/MkInfo.vue'; import { langs } from '@/config.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; @@ -234,6 +267,7 @@ const lang = ref(miLocalStorage.getItem('lang')); const fontSize = ref(miLocalStorage.getItem('fontSize')); const cornerRadius = ref(miLocalStorage.getItem('cornerRadius')); const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null); +const dataSaver = ref(defaultStore.state.dataSaver); async function reloadAsk() { const { canceled } = await os.confirm({ @@ -250,6 +284,7 @@ const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serve const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover')); const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter')); const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize')); +const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction')); const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes')); const clickToOpen = computed(defaultStore.makeGetterSetter('clickToOpen')); const showBots = computed(defaultStore.makeGetterSetter('tlWithBots')); @@ -267,7 +302,6 @@ const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('dis const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds')); const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages')); const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia')); -const enableDataSaverMode = computed(defaultStore.makeGetterSetter('enableDataSaverMode')); const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab')); const nsfw = computed(defaultStore.makeGetterSetter('nsfw')); const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm')); @@ -289,10 +323,12 @@ const showTickerOnReplies = computed(defaultStore.makeGetterSetter('showTickerOn const noteDesign = computed(defaultStore.makeGetterSetter('noteDesign')); const uncollapseCW = computed(defaultStore.makeGetterSetter('uncollapseCW')); const expandLongNote = computed(defaultStore.makeGetterSetter('expandLongNote')); +const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect')); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); miLocalStorage.removeItem('locale'); + miLocalStorage.removeItem('localeVersion'); }); watch(fontSize, () => { @@ -338,9 +374,11 @@ watch([ overridedDeviceKind, mediaListWithOneImageAppearance, reactionsDisplaySize, + limitWidthOfReaction, highlightSensitiveMedia, keepScreenOn, disableStreamingTimeline, + enableSeasonalScreenEffect, ], async () => { await reloadAsk(); }); @@ -419,9 +457,31 @@ function testNotification(): void { }, 300); } -const headerActions = $computed(() => []); +function enableAllDataSaver() { + const g = { ...defaultStore.state.dataSaver }; + + Object.keys(g).forEach((key) => { g[key] = true; }); + + dataSaver.value = g; +} + +function disableAllDataSaver() { + const g = { ...defaultStore.state.dataSaver }; + + Object.keys(g).forEach((key) => { g[key] = false; }); + + dataSaver.value = g; +} + +watch(dataSaver, (to) => { + defaultStore.set('dataSaver', to); +}, { + deep: true, +}); + +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.general, diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index 31bfd7e732..7ca1faf406 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { ref, computed } from 'vue'; import MkButton from '@/components/MkButton.vue'; import FormSection from '@/components/form/section.vue'; import MkFolder from '@/components/MkFolder.vue'; @@ -233,9 +233,9 @@ const importAntennas = async (ev) => { os.api('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError); }; -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.importAndExport, diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 04c1f2dbf2..558aed67a5 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -32,13 +32,11 @@ import { i18n } from '@/i18n.js'; import MkInfo from '@/components/MkInfo.vue'; import MkSuperMenu from '@/components/MkSuperMenu.vue'; import { signout, $i } from '@/account.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; +import { clearCache } from '@/scripts/clear-cache.js'; import { instance } from '@/instance.js'; import { useRouter } from '@/router.js'; import { definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import * as os from '@/os.js'; -import { miLocalStorage } from '@/local-storage.js'; -import { fetchCustomEmojis } from '@/custom-emojis.js'; const indexInfo = { title: i18n.ts.settings, @@ -51,14 +49,14 @@ const childInfo = ref(null); const router = useRouter(); -let narrow = $ref(false); +const narrow = ref(false); const NARROW_THRESHOLD = 600; -let currentPage = $computed(() => router.currentRef.value.child); +const currentPage = computed(() => router.currentRef.value.child); const ro = new ResizeObserver((entries, observer) => { if (entries.length === 0) return; - narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD; + narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD; }); const menuDef = computed(() => [{ @@ -67,37 +65,37 @@ const menuDef = computed(() => [{ icon: 'ph-user ph-bold ph-lg', text: i18n.ts.profile, to: '/settings/profile', - active: currentPage?.route.name === 'profile', + active: currentPage.value?.route.name === 'profile', }, { icon: 'ph-lock ph-bold ph-lg-open', text: i18n.ts.privacy, to: '/settings/privacy', - active: currentPage?.route.name === 'privacy', + active: currentPage.value?.route.name === 'privacy', }, { icon: 'ph-smiley ph-bold ph-lg', - text: i18n.ts.reaction, - to: '/settings/reaction', - active: currentPage?.route.name === 'reaction', + text: i18n.ts.emojiPicker, + to: '/settings/emoji-picker', + active: currentPage.value?.route.name === 'emojiPicker', }, { icon: 'ph-cloud ph-bold ph-lg', text: i18n.ts.drive, to: '/settings/drive', - active: currentPage?.route.name === 'drive', + active: currentPage.value?.route.name === 'drive', }, { icon: 'ph-bell ph-bold ph-lg', text: i18n.ts.notifications, to: '/settings/notifications', - active: currentPage?.route.name === 'notifications', + active: currentPage.value?.route.name === 'notifications', }, { icon: 'ph-envelope ph-bold ph-lg', text: i18n.ts.email, to: '/settings/email', - active: currentPage?.route.name === 'email', + active: currentPage.value?.route.name === 'email', }, { icon: 'ph-lock ph-bold ph-lg', text: i18n.ts.security, to: '/settings/security', - active: currentPage?.route.name === 'security', + active: currentPage.value?.route.name === 'security', }], }, { title: i18n.ts.clientSettings, @@ -105,32 +103,32 @@ const menuDef = computed(() => [{ icon: 'ph-faders ph-bold ph-lg', text: i18n.ts.general, to: '/settings/general', - active: currentPage?.route.name === 'general', + active: currentPage.value?.route.name === 'general', }, { icon: 'ph-palette ph-bold ph-lg', text: i18n.ts.theme, to: '/settings/theme', - active: currentPage?.route.name === 'theme', + active: currentPage.value?.route.name === 'theme', }, { icon: 'ph-list ph-bold ph-lg-2', text: i18n.ts.navbar, to: '/settings/navbar', - active: currentPage?.route.name === 'navbar', + active: currentPage.value?.route.name === 'navbar', }, { icon: 'ph-equals ph-bold ph-lg', text: i18n.ts.statusbar, to: '/settings/statusbar', - active: currentPage?.route.name === 'statusbar', + active: currentPage.value?.route.name === 'statusbar', }, { icon: 'ph-music-notes ph-bold ph-lg', text: i18n.ts.sounds, to: '/settings/sounds', - active: currentPage?.route.name === 'sounds', + active: currentPage.value?.route.name === 'sounds', }, { icon: 'ph-plug ph-bold ph-lg', text: i18n.ts.plugins, to: '/settings/plugin', - active: currentPage?.route.name === 'plugin', + active: currentPage.value?.route.name === 'plugin', }], }, { title: i18n.ts.otherSettings, @@ -138,56 +136,50 @@ const menuDef = computed(() => [{ icon: 'ph-seal-check ph-bold ph-lg', text: i18n.ts.roles, to: '/settings/roles', - active: currentPage?.route.name === 'roles', + active: currentPage.value?.route.name === 'roles', }, { icon: 'ph-prohibit ph-bold ph-lg', text: i18n.ts.muteAndBlock, to: '/settings/mute-block', - active: currentPage?.route.name === 'mute-block', + active: currentPage.value?.route.name === 'mute-block', }, { icon: 'ph-key ph-bold ph-lg', text: 'API', to: '/settings/api', - active: currentPage?.route.name === 'api', + active: currentPage.value?.route.name === 'api', }, { icon: 'ph-webhooks-logo ph-bold ph-lg', text: 'Webhook', to: '/settings/webhook', - active: currentPage?.route.name === 'webhook', + active: currentPage.value?.route.name === 'webhook', }, { icon: 'ph-package ph-bold ph-lg', text: i18n.ts.importAndExport, to: '/settings/import-export', - active: currentPage?.route.name === 'import-export', + active: currentPage.value?.route.name === 'import-export', }, { icon: 'ph-airplane ph-bold ph-lg', text: `${i18n.ts.accountMigration}`, to: '/settings/migration', - active: currentPage?.route.name === 'migration', + active: currentPage.value?.route.name === 'migration', }, { icon: 'ph-dots-three ph-bold ph-lg', text: i18n.ts.other, to: '/settings/other', - active: currentPage?.route.name === 'other', + active: currentPage.value?.route.name === 'other', }], }, { items: [{ icon: 'ph-floppy-disk ph-bold ph-lg', text: i18n.ts.preferencesBackups, to: '/settings/preferences-backups', - active: currentPage?.route.name === 'preferences-backups', + active: currentPage.value?.route.name === 'preferences-backups', }, { type: 'button', icon: 'ph-trash ph-bold ph-lg', text: i18n.ts.clearCache, action: async () => { - os.waiting(); - miLocalStorage.removeItem('locale'); - miLocalStorage.removeItem('theme'); - miLocalStorage.removeItem('emojis'); - miLocalStorage.removeItem('lastEmojisFetchedAt'); - await fetchCustomEmojis(true); - unisonReload(); + await clearCache(); }, }, { type: 'button', @@ -205,23 +197,23 @@ const menuDef = computed(() => [{ }], }]); -watch($$(narrow), () => { +watch(narrow, () => { }); onMounted(() => { ro.observe(el.value); - narrow = el.value.offsetWidth < NARROW_THRESHOLD; + narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; - if (!narrow && currentPage?.route.name == null) { + if (!narrow.value && currentPage.value?.route.name == null) { router.replace('/settings/profile'); } }); onActivated(() => { - narrow = el.value.offsetWidth < NARROW_THRESHOLD; + narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; - if (!narrow && currentPage?.route.name == null) { + if (!narrow.value && currentPage.value?.route.name == null) { router.replace('/settings/profile'); } }); @@ -231,7 +223,7 @@ onUnmounted(() => { }); watch(router.currentRef, (to) => { - if (to.route.name === 'settings' && to.child?.route.name == null && !narrow) { + if (to.route.name === 'settings' && to.child?.route.name == null && !narrow.value) { router.replace('/settings/profile'); } }); @@ -243,12 +235,13 @@ provideMetadataReceiver((info) => { childInfo.value = null; } else { childInfo.value = info; + INFO.value.needWideArea = info.value.needWideArea ?? undefined; } }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(INFO); // w 890 diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index 32aa9bd754..6419d23a7a 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -9,7 +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/> + <XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-message-off"></i></template> + <template #label>{{ i18n.ts.hardWordMute }}</template> + + <XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/> </MkFolder> <MkFolder> @@ -119,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import XInstanceMute from './mute-block.instance-mute.vue'; import XWordMute from './mute-block.word-mute.vue'; import MkPagination from '@/components/MkPagination.vue'; @@ -129,6 +136,7 @@ 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 MkFolder from '@/components/MkFolder.vue'; const renoteMutingPagination = { @@ -146,9 +154,9 @@ const blockingPagination = { limit: 10, }; -let expandedRenoteMuteItems = $ref([]); -let expandedMuteItems = $ref([]); -let expandedBlockItems = $ref([]); +const expandedRenoteMuteItems = ref([]); +const expandedMuteItems = ref([]); +const expandedBlockItems = ref([]); async function unrenoteMute(user, ev) { os.popupMenu([{ @@ -184,32 +192,40 @@ async function unblock(user, ev) { } async function toggleRenoteMuteItem(item) { - if (expandedRenoteMuteItems.includes(item.id)) { - expandedRenoteMuteItems = expandedRenoteMuteItems.filter(x => x !== item.id); + if (expandedRenoteMuteItems.value.includes(item.id)) { + expandedRenoteMuteItems.value = expandedRenoteMuteItems.value.filter(x => x !== item.id); } else { - expandedRenoteMuteItems.push(item.id); + expandedRenoteMuteItems.value.push(item.id); } } async function toggleMuteItem(item) { - if (expandedMuteItems.includes(item.id)) { - expandedMuteItems = expandedMuteItems.filter(x => x !== item.id); + if (expandedMuteItems.value.includes(item.id)) { + expandedMuteItems.value = expandedMuteItems.value.filter(x => x !== item.id); } else { - expandedMuteItems.push(item.id); + expandedMuteItems.value.push(item.id); } } async function toggleBlockItem(item) { - if (expandedBlockItems.includes(item.id)) { - expandedBlockItems = expandedBlockItems.filter(x => x !== item.id); + if (expandedBlockItems.value.includes(item.id)) { + expandedBlockItems.value = expandedBlockItems.value.filter(x => x !== item.id); } else { - expandedBlockItems.push(item.id); + expandedBlockItems.value.push(item.id); } } -const headerActions = $computed(() => []); +async function saveMutedWords(mutedWords: (string | string[])[]) { + await os.api('i/update', { mutedWords }); +} + +async function saveHardMutedWords(hardMutedWords: (string | string[])[]) { + await os.api('i/update', { hardMutedWords }); +} + +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.muteAndBlock, diff --git a/packages/frontend/src/pages/settings/mute-block.word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue index 4b6d27d8f2..96ee48cdba 100644 --- a/packages/frontend/src/pages/settings/mute-block.word-mute.vue +++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue @@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, watch } from 'vue'; import MkTextarea from '@/components/MkTextarea.vue'; -import MkKeyValue from '@/components/MkKeyValue.vue'; import MkButton from '@/components/MkButton.vue'; -import MkInfo from '@/components/MkInfo.vue'; -import MkTab from '@/components/MkTab.vue'; import * as os from '@/os.js'; -import number from '@/filters/number.js'; -import { defaultStore } from '@/store.js'; -import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; + +const props = defineProps<{ + muted: (string[] | string)[]; +}>(); + +const emit = defineEmits<{ + (ev: 'save', value: (string[] | string)[]): void; +}>(); const render = (mutedWords) => mutedWords.map(x => { if (Array.isArray(x)) { @@ -37,8 +38,7 @@ const render = (mutedWords) => mutedWords.map(x => { } }).join('\n'); -const tab = ref('soft'); -const mutedWords = ref(render($i!.mutedWords)); +const mutedWords = ref(render(props.muted)); const changed = ref(false); watch(mutedWords, () => { @@ -85,9 +85,7 @@ async function save() { return; } - await os.api('i/update', { - mutedWords: parsed, - }); + emit('save', parsed); changed.value = false; } diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index 51a826ef3e..0112543781 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -57,7 +57,6 @@ import { defaultStore } from '@/store.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { deepClone } from '@/scripts/clone.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -115,9 +114,9 @@ watch(menuDisplay, async () => { await reloadAsk(); }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.navbar, diff --git a/packages/frontend/src/pages/settings/notifications.notification-config.vue b/packages/frontend/src/pages/settings/notifications.notification-config.vue index 202b018efd..06686c3204 100644 --- a/packages/frontend/src/pages/settings/notifications.notification-config.vue +++ b/packages/frontend/src/pages/settings/notifications.notification-config.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkSelect from '@/components/MkSelect.vue'; import MkButton from '@/components/MkButton.vue'; @@ -41,10 +41,10 @@ const emit = defineEmits<{ (ev: 'update', result: any): void; }>(); -let type = $ref(props.value.type); -let userListId = $ref(props.value.userListId); +const type = ref(props.value.type); +const userListId = ref(props.value.userListId); function save() { - emit('update', { type, userListId }); + emit('update', { type: type.value, userListId: userListId.value }); } </script> diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index e5a3ddf88b..0bdfbdf741 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -55,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; +import { shallowRef, computed } from 'vue'; import XNotificationConfig from './notifications.notification-config.vue'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; @@ -68,11 +68,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; import { notificationTypes } from '@/const.js'; -const nonConfigurableNotificationTypes = ['note']; +const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned']; -let allowButton = $shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>(); -let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer); -let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage || false); +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'); async function readAllUnreadNotes() { @@ -95,14 +95,14 @@ async function updateReceiveConfig(type, value) { } function onChangeSendReadMessage(v: boolean) { - if (!pushRegistrationInServer) return; + if (!pushRegistrationInServer.value) return; os.apiWithDialog('sw/update-registration', { - endpoint: pushRegistrationInServer.endpoint, + endpoint: pushRegistrationInServer.value.endpoint, sendReadMessage: v, }).then(res => { - if (!allowButton) return; - allowButton.pushRegistrationInServer = res; + if (!allowButton.value) return; + allowButton.value.pushRegistrationInServer = res; }); } @@ -110,9 +110,9 @@ function testNotification(): void { os.api('notifications/test-notification'); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.notifications, diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 758e41047d..efda0c00b3 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -188,9 +188,9 @@ watch([ await reloadAsk(); }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.other, diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue index 1546223f12..be8db548a4 100644 --- a/packages/frontend/src/pages/settings/plugin.install.vue +++ b/packages/frontend/src/pages/settings/plugin.install.vue @@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <FormInfo warn>{{ i18n.ts._plugin.installWarn }}</FormInfo> - <MkTextarea v-model="code" tall> + <MkCodeEditor v-model="code" lang="is"> <template #label>{{ i18n.ts.code }}</template> - </MkTextarea> + </MkCodeEditor> <div> <MkButton :disabled="code == null" primary inline @click="install"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.install }}</MkButton> @@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, ref } from 'vue'; -import MkTextarea from '@/components/MkTextarea.vue'; +import { nextTick, ref, computed } from 'vue'; +import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkButton from '@/components/MkButton.vue'; import FormInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; @@ -49,9 +49,9 @@ async function install() { } } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts._plugin.install, diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue index 7447cef431..5b5c282f39 100644 --- a/packages/frontend/src/pages/settings/plugin.vue +++ b/packages/frontend/src/pages/settings/plugin.vue @@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, ref } from 'vue'; +import { nextTick, ref, computed } from 'vue'; import FormLink from '@/components/form/link.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import FormSection from '@/components/form/section.vue'; @@ -121,9 +121,9 @@ function changeActive(plugin, active) { }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.plugins, diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index f7768ebe5c..c7538f3a1b 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -54,22 +54,24 @@ import { miLocalStorage } from '@/local-storage.js'; const { t, ts } = i18n; const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ + 'collapseRenotes', 'menu', 'visibility', 'localOnly', 'statusbars', 'widgets', 'tl', + 'pinnedUserLists', 'overridedDeviceKind', 'serverDisconnectedBehavior', - 'collapseRenotes', - 'showNoteActionsOnlyHover', 'nsfw', + 'highlightSensitiveMedia', 'animation', 'animatedMfm', 'advancedMfm', 'loadRawImages', 'imageNewTab', + 'dataSaver', 'disableShowingAnimatedImages', 'emojiStyle', 'disableDrawer', @@ -81,18 +83,37 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'useReactionPickerForContextMenu', 'showGapBetweenNotesInTimeline', 'instanceTicker', - 'reactionPickerSize', - 'reactionPickerWidth', - 'reactionPickerHeight', - 'reactionPickerUseDrawerForMobile', + 'emojiPickerScale', + 'emojiPickerWidth', + 'emojiPickerHeight', + 'emojiPickerUseDrawerForMobile', 'defaultSideView', 'menuDisplay', 'reportError', 'squareAvatars', + 'showAvatarDecorations', 'numberOfPageCache', + 'showNoteActionsOnlyHover', + 'showClipButtonInNoteFooter', + 'reactionsDisplaySize', + 'forceShowAds', 'numberOfReplies', 'aiChanMode', + 'devMode', 'mediaListWithOneImageAppearance', + 'notificationPosition', + 'notificationStackAxis', + 'enableCondensedLineForAcct', + 'keepScreenOn', + 'defaultWithReplies', + 'disableStreamingTimeline', + 'useGroupedNotifications', + 'sound_masterVolume', + 'sound_note', + 'sound_noteMy', + 'sound_notification', + 'sound_antenna', + 'sound_channel', ]; const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [ 'lightTheme', @@ -395,7 +416,7 @@ function menu(ev: MouseEvent, profileId: string) { icon: 'ph-download ph-bold ph-lg', href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })), download: `${profiles.value[profileId].name}.json`, - }, null, { + }, { type: 'divider' }, { text: ts.rename, icon: 'ph-textbox ph-bold ph-lg', action: () => rename(profileId), @@ -403,7 +424,7 @@ function menu(ev: MouseEvent, profileId: string) { text: ts._preferencesBackups.save, icon: 'ph-floppy-disk ph-bold ph-lg', action: () => save(profileId), - }, null, { + }, { type: 'divider' }, { text: ts.delete, icon: 'ph-trash ph-bold ph-lg', action: () => deleteProfile(profileId), diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index dae4251892..62056ff8a6 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -13,12 +13,18 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template> </MkSwitch> - <MkSelect v-model="ffVisibility" @update:modelValue="save()"> - <template #label>{{ i18n.ts.ffVisibility }}</template> + <MkSelect v-model="followingVisibility" @update:modelValue="save()"> + <template #label>{{ i18n.ts.followingVisibility }}</template> + <option value="public">{{ i18n.ts._ffVisibility.public }}</option> + <option value="followers">{{ i18n.ts._ffVisibility.followers }}</option> + <option value="private">{{ i18n.ts._ffVisibility.private }}</option> + </MkSelect> + + <MkSelect v-model="followersVisibility" @update:modelValue="save()"> + <template #label>{{ i18n.ts.followersVisibility }}</template> <option value="public">{{ i18n.ts._ffVisibility.public }}</option> <option value="followers">{{ i18n.ts._ffVisibility.followers }}</option> <option value="private">{{ i18n.ts._ffVisibility.private }}</option> - <template #caption>{{ i18n.ts.ffVisibilityDescription }}</template> </MkSelect> <MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()"> @@ -66,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import FormSection from '@/components/form/section.vue'; @@ -77,36 +83,38 @@ import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let isLocked = $ref($i.isLocked); -let autoAcceptFollowed = $ref($i.autoAcceptFollowed); -let noCrawle = $ref($i.noCrawle); -let isExplorable = $ref($i.isExplorable); -let noindex = $ref($i.noindex); -let hideOnlineStatus = $ref($i.hideOnlineStatus); -let publicReactions = $ref($i.publicReactions); -let ffVisibility = $ref($i.ffVisibility); +const isLocked = ref($i.isLocked); +const autoAcceptFollowed = ref($i.autoAcceptFollowed); +const noCrawle = ref($i.noCrawle); +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); -let defaultNoteVisibility = $computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); -let defaultNoteLocalOnly = $computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); -let rememberNoteVisibility = $computed(defaultStore.makeGetterSetter('rememberNoteVisibility')); -let keepCw = $computed(defaultStore.makeGetterSetter('keepCw')); +const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); +const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); +const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility')); +const keepCw = computed(defaultStore.makeGetterSetter('keepCw')); function save() { os.api('i/update', { - isLocked: !!isLocked, - autoAcceptFollowed: !!autoAcceptFollowed, - noCrawle: !!noCrawle, - isExplorable: !!isExplorable, - noindex: !!noindex, - hideOnlineStatus: !!hideOnlineStatus, - publicReactions: !!publicReactions, - ffVisibility: ffVisibility, + isLocked: !!isLocked.value, + autoAcceptFollowed: !!autoAcceptFollowed.value, + noCrawle: !!noCrawle.value, + noindex: !!noindex.value, + isExplorable: !!isExplorable.value, + hideOnlineStatus: !!hideOnlineStatus.value, + publicReactions: !!publicReactions.value, + followingVisibility: followingVisibility.value, + followersVisibility: followersVisibility.value, }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.privacy, diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue deleted file mode 100644 index d0c50d59cd..0000000000 --- a/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue +++ /dev/null @@ -1,114 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and other misskey contributors -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<MkModalWindow - ref="dialog" - :width="400" - :height="450" - @close="cancel" - @closed="emit('closed')" -> - <template #header>{{ i18n.ts.avatarDecorations }}</template> - - <div> - <MkSpacer :marginMin="20" :marginMax="28"> - <div style="text-align: center;"> - <div :class="$style.name">{{ decoration.name }}</div> - <MkAvatar style="width: 64px; height: 64px; margin-bottom: 20px;" :user="$i" :decoration="{ url: decoration.url, angle, flipH }" forceShowDecoration/> - </div> - <div class="_gaps_s"> - <MkRange v-model="angle" continuousUpdate :min="-0.5" :max="0.5" :step="0.025" :textConverter="(v) => `${Math.floor(v * 360)}°`"> - <template #label>{{ i18n.ts.angle }}</template> - </MkRange> - <MkSwitch v-model="flipH"> - <template #label>{{ i18n.ts.flip }}</template> - </MkSwitch> - </div> - </MkSpacer> - - <div :class="$style.footer" class="_buttonsCenter"> - <MkButton v-if="using" primary rounded @click="attach"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.update }}</MkButton> - <MkButton v-if="using" rounded @click="detach"><i class="ph-x ph-bold ph-lg"></i> {{ i18n.ts.detach }}</MkButton> - <MkButton v-else primary rounded @click="attach"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.attach }}</MkButton> - </div> - </div> -</MkModalWindow> -</template> - -<script lang="ts" setup> -import { shallowRef, ref, computed } from 'vue'; -import MkButton from '@/components/MkButton.vue'; -import MkModalWindow from '@/components/MkModalWindow.vue'; -import MkSwitch from '@/components/MkSwitch.vue'; -import { i18n } from '@/i18n.js'; -import * as os from '@/os.js'; -import MkFolder from '@/components/MkFolder.vue'; -import MkInfo from '@/components/MkInfo.vue'; -import MkRange from '@/components/MkRange.vue'; -import { $i } from '@/account.js'; - -const props = defineProps<{ - decoration: { - id: string; - url: string; - } -}>(); - -const emit = defineEmits<{ - (ev: 'closed'): void; -}>(); - -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); -const using = computed(() => $i.avatarDecorations.some(x => x.id === props.decoration.id)); -const angle = ref(using.value ? $i.avatarDecorations.find(x => x.id === props.decoration.id).angle ?? 0 : 0); -const flipH = ref(using.value ? $i.avatarDecorations.find(x => x.id === props.decoration.id).flipH ?? false : false); - -function cancel() { - dialog.value.close(); -} - -async function attach() { - const decoration = { - id: props.decoration.id, - angle: angle.value, - flipH: flipH.value, - }; - await os.apiWithDialog('i/update', { - avatarDecorations: [decoration], - }); - $i.avatarDecorations = [decoration]; - - dialog.value.close(); -} - -async function detach() { - await os.apiWithDialog('i/update', { - avatarDecorations: [], - }); - $i.avatarDecorations = []; - - dialog.value.close(); -} -</script> - -<style lang="scss" module> -.name { - position: relative; - z-index: 10; - font-weight: bold; - margin-bottom: 28px; -} - -.footer { - position: sticky; - bottom: 0; - left: 0; - padding: 12px; - border-top: solid 0.5px var(--divider); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); -} -</style> diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index b585333bbd..4bae635d05 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -5,20 +5,25 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> - <div :class="$style.avatarAndBanner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> + <div class="_panel"> + <div :class="$style.banner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> + <MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton> + <MkButton primary rounded :class="$style.backgroundEdit" @click="changeBackground">{{ i18n.ts._profile.changeBackground }}</MkButton> + </div> <div :class="$style.avatarContainer"> <MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration @click="changeAvatar"/> - <MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton> + <div class="_buttonsCenter"> + <MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton> + <MkButton primary rounded link to="/settings/avatar-decoration">{{ i18n.ts.decorate }} <i class="ph-sparkle ph-bold ph-lg"></i></MkButton> + </div> </div> - <MkButton primary rounded :class="$style.backgroundEdit" @click="changeBackground">{{ i18n.ts._profile.changeBackground }}</MkButton> - <MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton> </div> - <MkInput v-model="profile.name" :max="30" manualSave> + <MkInput v-model="profile.name" :max="30" manualSave :mfmAutocomplete="['emoji']"> <template #label>{{ i18n.ts._profile.name }}</template> </MkInput> - <MkTextarea v-model="profile.description" :max="500" tall manualSave> + <MkTextarea v-model="profile.description" :max="500" tall manualSave mfmAutocomplete :mfmPreview="true"> <template #label>{{ i18n.ts._profile.description }}</template> <template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template> </MkTextarea> @@ -90,24 +95,6 @@ SPDX-License-Identifier: AGPL-3.0-only </FormSlot> <MkFolder> - <template #icon><i class="ph-sparkle ph-bold ph-lg"></i></template> - <template #label>{{ i18n.ts.avatarDecorations }}</template> - - <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-gap: 12px;"> - <div - v-for="avatarDecoration in avatarDecorations" - :key="avatarDecoration.id" - :class="[$style.avatarDecoration, { [$style.avatarDecorationActive]: $i.avatarDecorations.some(x => x.id === avatarDecoration.id) }]" - @click="openDecoration(avatarDecoration)" - > - <div :class="$style.avatarDecorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div> - <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decoration="{ url: avatarDecoration.url }" forceShowDecoration/> - <i v-if="avatarDecoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => avatarDecoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.avatarDecorationLock" class="ph-lock ph-bold ph-lg"></i> - </div> - </div> - </MkFolder> - - <MkFolder> <template #label>{{ i18n.ts.advancedSettings }}</template> <div class="_gaps_m"> @@ -129,10 +116,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, reactive, ref, watch, defineAsyncComponent, onMounted, onUnmounted } from 'vue'; +import { computed, reactive, ref, watch, defineAsyncComponent } from 'vue'; 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 MkSelect from '@/components/MkSelect.vue'; import FormSplit from '@/components/form/split.vue'; @@ -147,11 +133,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { defaultStore } from '@/store.js'; import MkInfo from '@/components/MkInfo.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance')); -let avatarDecorations: any[] = $ref([]); const now = new Date(); @@ -182,10 +168,6 @@ watch(() => profile, () => { const fields = ref($i?.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []); const fieldEditMode = ref(false); -os.api('get-avatar-decorations').then(_avatarDecorations => { - avatarDecorations = _avatarDecorations; -}); - function addField() { fields.value.push({ id: Math.random().toString(), @@ -318,15 +300,9 @@ function changeBackground(ev) { }); } -function openDecoration(avatarDecoration) { - os.popup(defineAsyncComponent(() => import('./profile.avatar-decoration-dialog.vue')), { - decoration: avatarDecoration, - }, {}, 'closed'); -} - -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.profile, @@ -335,19 +311,19 @@ definePageMetadata({ </script> <style lang="scss" module> -.avatarAndBanner { +.banner { position: relative; + height: 130px; background-size: cover; background-position: center; - border: solid 1px var(--divider); - border-radius: var(--radius); + border-bottom: solid 1px var(--divider); overflow: clip; } .avatarContainer { - display: inline-block; + margin-top: -50px; + padding-bottom: 16px; text-align: center; - padding: 16px; } .avatar { @@ -364,7 +340,7 @@ definePageMetadata({ } .backgroundEdit { position: absolute; - top: 103px; + top: 95px; right: 16px; } @@ -423,33 +399,4 @@ definePageMetadata({ .dragItemForm { flex-grow: 1; } - -.avatarDecoration { - cursor: pointer; - padding: 16px 16px 28px 16px; - border: solid 2px var(--divider); - border-radius: var(--radius-sm); - text-align: center; - font-size: 90%; - overflow: clip; - contain: content; -} - -.avatarDecorationActive { - background-color: var(--accentedBg); - border-color: var(--accent); -} - -.avatarDecorationName { - position: relative; - z-index: 10; - font-weight: bold; - margin-bottom: 20px; -} - -.avatarDecorationLock { - position: absolute; - bottom: 12px; - right: 12px; -} </style> diff --git a/packages/frontend/src/pages/settings/reaction.vue b/packages/frontend/src/pages/settings/reaction.vue deleted file mode 100644 index 213e73b526..0000000000 --- a/packages/frontend/src/pages/settings/reaction.vue +++ /dev/null @@ -1,196 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and other misskey contributors -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<div class="_gaps_m"> - <FromSlot> - <template #label>{{ i18n.ts.reactionSettingDescription }}</template> - <div v-panel style="border-radius: var(--radius-sm);"> - <Sortable v-model="reactions" :class="$style.reactions" :itemKey="item => item" :animation="150" :delay="100" :delayOnTouchOnly="true"> - <template #item="{element}"> - <button class="_button" :class="$style.reactionsItem" @click="remove(element, $event)"> - <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/> - <MkEmoji v-else :emoji="element" :normal="true"/> - </button> - </template> - <template #footer> - <button class="_button" :class="$style.reactionsAdd" @click="chooseEmoji"><i class="ph-plus ph-bold ph-lg"></i></button> - </template> - </Sortable> - </div> - <template #caption>{{ i18n.ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ i18n.ts.preview }}</button></template> - </FromSlot> - - <FromSlot> - <template #label>{{ i18n.ts.defaultLike }}</template> - <MkCustomEmoji v-if="like && like.startsWith(':')" style="max-height: 3em; font-size: 1.1em;" :useOriginalSize="false" :class="$style.reaction" :name="like" :normal="true" :noStyle="true"/> - <MkEmoji v-else-if="like && !like.startsWith(':')" :emoji="like" style="max-height: 3em; font-size: 1.1em;" :normal="true" :noStyle="true"/> - <span v-else-if="!like">{{ i18n.ts.notSet }}</span> - <div class="_buttons" style="padding-top: 8px;"> - <MkButton rounded :small="true" inline @click="chooseNewLike"><i class="ph-smiley ph-bold ph-lg"></i> Change</MkButton> - <MkButton rounded :small="true" inline @click="resetLike"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> Reset</MkButton> - </div> - </FromSlot> - - <MkRadios v-model="reactionPickerSize"> - <template #label>{{ i18n.ts.size }}</template> - <option :value="1">{{ i18n.ts.small }}</option> - <option :value="2">{{ i18n.ts.medium }}</option> - <option :value="3">{{ i18n.ts.large }}</option> - </MkRadios> - <MkRadios v-model="reactionPickerWidth"> - <template #label>{{ i18n.ts.numberOfColumn }}</template> - <option :value="1">5</option> - <option :value="2">6</option> - <option :value="3">7</option> - <option :value="4">8</option> - <option :value="5">9</option> - </MkRadios> - <MkRadios v-model="reactionPickerHeight"> - <template #label>{{ i18n.ts.height }}</template> - <option :value="1">{{ i18n.ts.small }}</option> - <option :value="2">{{ i18n.ts.medium }}</option> - <option :value="3">{{ i18n.ts.large }}</option> - <option :value="4">{{ i18n.ts.large }}+</option> - </MkRadios> - - <MkSwitch v-model="reactionPickerUseDrawerForMobile"> - {{ i18n.ts.useDrawerReactionPickerForMobile }} - <template #caption>{{ i18n.ts.needReloadToApply }}</template> - </MkSwitch> - - <FormSection> - <div class="_buttons"> - <MkButton inline @click="preview"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.preview }}</MkButton> - <MkButton inline danger @click="setDefault"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> {{ i18n.ts.default }}</MkButton> - </div> - </FormSection> -</div> -</template> - -<script lang="ts" setup> -import { defineAsyncComponent, watch } from 'vue'; -import Sortable from 'vuedraggable'; -import MkRadios from '@/components/MkRadios.vue'; -import FromSlot from '@/components/form/slot.vue'; -import MkButton from '@/components/MkButton.vue'; -import FormSection from '@/components/form/section.vue'; -import MkSwitch from '@/components/MkSwitch.vue'; -import * as os from '@/os.js'; -import { defaultStore } from '@/store.js'; -import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { deepClone } from '@/scripts/clone.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; - -let reactions = $ref(deepClone(defaultStore.state.reactions)); -const like = $computed(defaultStore.makeGetterSetter('like')); - -const reactionPickerSize = $computed(defaultStore.makeGetterSetter('reactionPickerSize')); -const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth')); -const reactionPickerHeight = $computed(defaultStore.makeGetterSetter('reactionPickerHeight')); -const reactionPickerUseDrawerForMobile = $computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile')); - -async function reloadAsk() { - const { canceled } = await os.confirm({ - type: 'info', - text: i18n.ts.reloadToApplySetting, - }); - if (canceled) return; - - unisonReload(); -} - -function save() { - defaultStore.set('reactions', reactions); -} - -function remove(reaction, ev: MouseEvent) { - os.popupMenu([{ - text: i18n.ts.remove, - action: () => { - reactions = reactions.filter(x => x !== reaction); - }, - }], ev.currentTarget ?? ev.target); -} - -function preview(ev: MouseEvent) { - os.popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { - asReactionPicker: true, - src: ev.currentTarget ?? ev.target, - }, {}, 'closed'); -} - -async function setDefault() { - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.resetAreYouSure, - }); - if (canceled) return; - - reactions = deepClone(defaultStore.def.reactions.default); -} - -function chooseEmoji(ev: MouseEvent) { - os.pickEmoji(ev.currentTarget ?? ev.target, { - showPinned: false, - }).then(emoji => { - if (!reactions.includes(emoji)) { - reactions.push(emoji); - } - }); -} - -function chooseNewLike(ev: MouseEvent) { - os.pickEmoji(ev.currentTarget ?? ev.target, { - showPinned: false, - }).then(async emoji => { - defaultStore.set('like', emoji as string); - await reloadAsk(); - }); -} - -async function resetLike() { - defaultStore.set('like', null); - await reloadAsk(); -} - -watch($$(reactions), () => { - save(); -}, { - deep: true, -}); - -const headerActions = $computed(() => []); - -const headerTabs = $computed(() => []); - -definePageMetadata({ - title: i18n.ts.reaction, - icon: 'ph-smiley ph-bold ph-lg', - action: { - icon: 'ph-eye ph-bold ph-lg', - handler: preview, - }, -}); -</script> - -<style lang="scss" module> -.reactions { - padding: 12px; - font-size: 1.1em; -} - -.reactionsItem { - display: inline-block; - padding: 8px; - cursor: move; -} - -.reactionsAdd { - display: inline-block; - padding: 8px; -} -</style> diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue index 291be4f3be..716b168c92 100644 --- a/packages/frontend/src/pages/settings/roles.vue +++ b/packages/frontend/src/pages/settings/roles.vue @@ -23,21 +23,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, reactive, watch } from 'vue'; -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 MkSelect from '@/components/MkSelect.vue'; -import FormSplit from '@/components/form/split.vue'; -import MkFolder from '@/components/MkFolder.vue'; -import FormSlot from '@/components/form/slot.vue'; +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 { definePageMetadata } from '@/scripts/page-metadata.js'; -import { defaultStore } from '@/store.js'; import MkRolePreview from '@/components/MkRolePreview.vue'; function save() { @@ -46,9 +37,9 @@ function save() { }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.roles, diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue index 97bb84b655..9ae479e6e4 100644 --- a/packages/frontend/src/pages/settings/security.vue +++ b/packages/frontend/src/pages/settings/security.vue @@ -40,6 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { computed } from 'vue'; import X2fa from './2fa.vue'; import FormSection from '@/components/form/section.vue'; import FormSlot from '@/components/form/slot.vue'; @@ -97,9 +98,9 @@ async function regenerateToken() { }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.security, diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue index 514ead4de1..a43ffb1f0b 100644 --- a/packages/frontend/src/pages/settings/sounds.sound.vue +++ b/packages/frontend/src/pages/settings/sounds.sound.vue @@ -7,8 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <MkSelect v-model="type"> <template #label>{{ i18n.ts.sound }}</template> - <option v-for="x in soundsTypes" :key="x" :value="x">{{ x == null ? i18n.ts.none : x }}</option> + <option v-for="x in soundsTypes" :key="x ?? 'null'" :value="x">{{ getSoundTypeName(x) }}</option> </MkSelect> + <div v-if="type === '_driveFile_'" :class="$style.fileSelectorRoot"> + <MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton> + <div :class="['_nowrap', !fileUrl && $style.fileNotSelected]">{{ friendlyFileName }}</div> + </div> <MkRange v-model="volume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`"> <template #label>{{ i18n.ts.volume }}</template> </MkRange> @@ -21,30 +25,149 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; +import type { SoundType } from '@/scripts/sound.js'; import MkSelect from '@/components/MkSelect.vue'; import MkButton from '@/components/MkButton.vue'; import MkRange from '@/components/MkRange.vue'; import { i18n } from '@/i18n.js'; -import { playFile, soundsTypes } from '@/scripts/sound.js'; +import * as os from '@/os.js'; +import { playFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js'; +import { selectFile } from '@/scripts/select-file.js'; const props = defineProps<{ - type: string; + type: SoundType; + fileId?: string; + fileUrl?: string; volume: number; }>(); const emit = defineEmits<{ - (ev: 'update', result: { type: string; volume: number; }): void; + (ev: 'update', result: { type: SoundType; fileId?: string; fileUrl?: string; volume: number; }): void; }>(); -let type = $ref(props.type); -let volume = $ref(props.volume); +const type = ref<SoundType>(props.type); +const fileId = ref(props.fileId); +const fileUrl = ref(props.fileUrl); +const fileName = ref<string>(''); +const volume = ref(props.volume); + +if (type.value === '_driveFile_' && fileId.value) { + const apiRes = await os.api('drive/files/show', { + fileId: fileId.value, + }); + fileName.value = apiRes.name; +} + +function getSoundTypeName(f: SoundType): string { + switch (f) { + case null: + return i18n.ts.none; + case '_driveFile_': + return i18n.ts._soundSettings.driveFile; + default: + return f; + } +} + +const friendlyFileName = computed<string>(() => { + if (fileName.value) { + return fileName.value; + } + if (fileUrl.value) { + return fileUrl.value; + } + + return i18n.ts._soundSettings.driveFileWarn; +}); + +function selectSound(ev) { + selectFile(ev.currentTarget ?? ev.target, i18n.ts._soundSettings.driveFile).then(async (file) => { + if (!file.type.startsWith('audio')) { + os.alert({ + type: 'warning', + title: i18n.ts._soundSettings.driveFileTypeWarn, + text: i18n.ts._soundSettings.driveFileTypeWarnDescription, + }); + return; + } + const duration = await getSoundDuration(file.url); + if (duration >= 2000) { + const { canceled } = await os.confirm({ + type: 'warning', + title: i18n.ts._soundSettings.driveFileDurationWarn, + text: i18n.ts._soundSettings.driveFileDurationWarnDescription, + okText: i18n.ts.continue, + cancelText: i18n.ts.cancel, + }); + if (canceled) return; + } + + fileUrl.value = file.url; + fileName.value = file.name; + fileId.value = file.id; + }); +} function listen() { - playFile(type, volume); + if (type.value === '_driveFile_' && (!fileUrl.value || !fileId.value)) { + os.alert({ + type: 'warning', + text: i18n.ts._soundSettings.driveFileWarn, + }); + return; + } + + playFile(type.value === '_driveFile_' ? { + type: '_driveFile_', + fileId: fileId.value as string, + fileUrl: fileUrl.value as string, + volume: volume.value, + } : { + type: type.value, + volume: volume.value, + }); } function save() { - emit('update', { type, volume }); + if (type.value === '_driveFile_' && !fileUrl.value) { + os.alert({ + type: 'warning', + text: i18n.ts._soundSettings.driveFileWarn, + }); + return; + } + + if (type.value !== '_driveFile_') { + fileUrl.value = undefined; + fileName.value = ''; + fileId.value = undefined; + } + + emit('update', { + type: type.value, + fileId: fileId.value, + fileUrl: fileUrl.value, + volume: volume.value, + }); + + os.success(); } </script> + +<style module> +.fileSelectorRoot { + display: flex; + align-items: center; + gap: 8px; +} + +.fileSelectorButton { + flex-shrink: 0; +} + +.fileNotSelected { + font-weight: 700; + color: var(--infoWarnFg); +} +</style> diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index 89241b8c02..bec41a6cec 100644 --- a/packages/frontend/src/pages/settings/sounds.vue +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -5,6 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> + <MkSwitch v-model="notUseSound"> + <template #label>{{ i18n.ts.notUseSound }}</template> + </MkSwitch> + <MkSwitch v-model="useSoundOnlyWhenActive"> + <template #label>{{ i18n.ts.useSoundOnlyWhenActive }}</template> + </MkSwitch> <MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`"> <template #label>{{ i18n.ts.masterVolume }}</template> </MkRange> @@ -12,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <template #label>{{ i18n.ts.sounds }}</template> <div class="_gaps_s"> - <MkFolder v-for="type in soundsKeys" :key="type"> + <MkFolder v-for="type in operationTypes" :key="type"> <template #label>{{ i18n.t('_sfx.' + type) }}</template> - <template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template> + <template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template> - <XSound :type="sounds[type].type" :volume="sounds[type].volume" @update="(res) => updated(type, res)"/> + <XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/> </MkFolder> </div> </FormSection> @@ -27,6 +33,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { Ref, computed, ref } from 'vue'; +import type { SoundType, OperationType } from '@/scripts/sound.js'; +import type { SoundStore } from '@/store.js'; import XSound from './sounds.sound.vue'; import MkRange from '@/components/MkRange.vue'; import MkButton from '@/components/MkButton.vue'; @@ -34,23 +42,39 @@ import FormSection from '@/components/form/section.vue'; import MkFolder from '@/components/MkFolder.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { operationTypes } from '@/scripts/sound.js'; import { defaultStore } from '@/store.js'; +import MkSwitch from '@/components/MkSwitch.vue'; +const notUseSound = computed(defaultStore.makeGetterSetter('sound_notUseSound')); +const useSoundOnlyWhenActive = computed(defaultStore.makeGetterSetter('sound_useSoundOnlyWhenActive')); const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume')); -const soundsKeys = ['note', 'noteMy', 'notification', 'antenna', 'channel'] as const; - -const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({ +const sounds = ref<Record<OperationType, Ref<SoundStore>>>({ note: defaultStore.reactiveState.sound_note, noteMy: defaultStore.reactiveState.sound_noteMy, notification: defaultStore.reactiveState.sound_notification, antenna: defaultStore.reactiveState.sound_antenna, channel: defaultStore.reactiveState.sound_channel, + reaction: defaultStore.reactiveState.sound_reaction, }); +function getSoundTypeName(f: SoundType): string { + switch (f) { + case null: + return i18n.ts.none; + case '_driveFile_': + return i18n.ts._soundSettings.driveFile; + default: + return f; + } +} + async function updated(type: keyof typeof sounds.value, sound) { - const v = { + const v: SoundStore = { type: sound.type, + fileId: sound.fileId, + fileUrl: sound.fileUrl, volume: sound.volume, }; @@ -66,9 +90,9 @@ function reset() { } } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.sounds, diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue index 1999c7e20f..ae6a2deaf9 100644 --- a/packages/frontend/src/pages/settings/statusbar.vue +++ b/packages/frontend/src/pages/settings/statusbar.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref, computed } from 'vue'; import { v4 as uuid } from 'uuid'; import XStatusbar from './statusbar.statusbar.vue'; import MkFolder from '@/components/MkFolder.vue'; @@ -27,11 +27,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; const statusbars = defaultStore.reactiveState.statusbars; -let userLists = $ref(); +const userLists = ref(); onMounted(() => { os.api('users/lists/list').then(res => { - userLists = res; + userLists.value = res; }); }); @@ -45,9 +45,9 @@ async function add() { }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.statusbar, diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue index 9931bf8a76..d3ba9bc0aa 100644 --- a/packages/frontend/src/pages/settings/theme.install.vue +++ b/packages/frontend/src/pages/settings/theme.install.vue @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> - <MkTextarea v-model="installThemeCode"> + <MkCodeEditor v-model="installThemeCode" lang="json5"> <template #label>{{ i18n.ts._theme.code }}</template> - </MkTextarea> + </MkCodeEditor> <div class="_buttons"> <MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.preview }}</MkButton> @@ -17,15 +17,15 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; -import MkTextarea from '@/components/MkTextarea.vue'; +import { ref, computed } from 'vue'; +import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkButton from '@/components/MkButton.vue'; import { parseThemeCode, previewTheme, installTheme } from '@/scripts/install-theme.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let installThemeCode = $ref(null); +const installThemeCode = ref(null); async function install(code: string): Promise<void> { try { @@ -55,9 +55,9 @@ async function install(code: string): Promise<void> { } } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts._theme.install, diff --git a/packages/frontend/src/pages/settings/theme.manage.vue b/packages/frontend/src/pages/settings/theme.manage.vue index 484e83be4b..a2ecd0574f 100644 --- a/packages/frontend/src/pages/settings/theme.manage.vue +++ b/packages/frontend/src/pages/settings/theme.manage.vue @@ -72,9 +72,9 @@ function uninstall() { os.success(); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts._theme.manage, diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue index eb69912609..58c0b8fc82 100644 --- a/packages/frontend/src/pages/settings/theme.vue +++ b/packages/frontend/src/pages/settings/theme.vue @@ -160,9 +160,9 @@ function setWallpaper(event) { }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.theme, diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index d65dcae538..f6e2f63317 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import MkInput from '@/components/MkInput.vue'; import FormSection from '@/components/form/section.vue'; import MkSwitch from '@/components/MkSwitch.vue'; @@ -62,36 +62,36 @@ const webhook = await os.api('i/webhooks/show', { webhookId: props.webhookId, }); -let name = $ref(webhook.name); -let url = $ref(webhook.url); -let secret = $ref(webhook.secret); -let active = $ref(webhook.active); +const name = ref(webhook.name); +const url = ref(webhook.url); +const secret = ref(webhook.secret); +const active = ref(webhook.active); -let event_follow = $ref(webhook.on.includes('follow')); -let event_followed = $ref(webhook.on.includes('followed')); -let event_note = $ref(webhook.on.includes('note')); -let event_reply = $ref(webhook.on.includes('reply')); -let event_renote = $ref(webhook.on.includes('renote')); -let event_reaction = $ref(webhook.on.includes('reaction')); -let event_mention = $ref(webhook.on.includes('mention')); +const event_follow = ref(webhook.on.includes('follow')); +const event_followed = ref(webhook.on.includes('followed')); +const event_note = ref(webhook.on.includes('note')); +const event_reply = ref(webhook.on.includes('reply')); +const event_renote = ref(webhook.on.includes('renote')); +const event_reaction = ref(webhook.on.includes('reaction')); +const event_mention = ref(webhook.on.includes('mention')); async function save(): Promise<void> { const events = []; - if (event_follow) events.push('follow'); - if (event_followed) events.push('followed'); - if (event_note) events.push('note'); - if (event_reply) events.push('reply'); - if (event_renote) events.push('renote'); - if (event_reaction) events.push('reaction'); - if (event_mention) events.push('mention'); + if (event_follow.value) events.push('follow'); + if (event_followed.value) events.push('followed'); + if (event_note.value) events.push('note'); + if (event_reply.value) events.push('reply'); + if (event_renote.value) events.push('renote'); + if (event_reaction.value) events.push('reaction'); + if (event_mention.value) events.push('mention'); os.apiWithDialog('i/webhooks/update', { - name, - url, - secret, + name: name.value, + url: url.value, + secret: secret.value, webhookId: props.webhookId, on: events, - active, + active: active.value, }); } @@ -109,9 +109,9 @@ async function del(): Promise<void> { router.push('/settings/webhook'); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: 'Edit webhook', diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue index f08f143d46..032796caf0 100644 --- a/packages/frontend/src/pages/settings/webhook.new.vue +++ b/packages/frontend/src/pages/settings/webhook.new.vue @@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref, computed } from 'vue'; import MkInput from '@/components/MkInput.vue'; import FormSection from '@/components/form/section.vue'; import MkSwitch from '@/components/MkSwitch.vue'; @@ -48,39 +48,39 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -let name = $ref(''); -let url = $ref(''); -let secret = $ref(''); +const name = ref(''); +const url = ref(''); +const secret = ref(''); -let event_follow = $ref(true); -let event_followed = $ref(true); -let event_note = $ref(true); -let event_reply = $ref(true); -let event_renote = $ref(true); -let event_reaction = $ref(true); -let event_mention = $ref(true); +const event_follow = ref(true); +const event_followed = ref(true); +const event_note = ref(true); +const event_reply = ref(true); +const event_renote = ref(true); +const event_reaction = ref(true); +const event_mention = ref(true); async function create(): Promise<void> { const events = []; - if (event_follow) events.push('follow'); - if (event_followed) events.push('followed'); - if (event_note) events.push('note'); - if (event_reply) events.push('reply'); - if (event_renote) events.push('renote'); - if (event_reaction) events.push('reaction'); - if (event_mention) events.push('mention'); + if (event_follow.value) events.push('follow'); + if (event_followed.value) events.push('followed'); + if (event_note.value) events.push('note'); + if (event_reply.value) events.push('reply'); + if (event_renote.value) events.push('renote'); + if (event_reaction.value) events.push('reaction'); + if (event_mention.value) events.push('mention'); os.apiWithDialog('i/webhooks/create', { - name, - url, - secret, + name: name.value, + url: url.value, + secret: secret.value, on: events, }); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: 'Create new webhook', diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue index e9a7f9a02e..c391458274 100644 --- a/packages/frontend/src/pages/settings/webhook.vue +++ b/packages/frontend/src/pages/settings/webhook.vue @@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { computed } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import FormSection from '@/components/form/section.vue'; import FormLink from '@/components/form/link.vue'; @@ -46,9 +46,9 @@ const pagination = { noPaging: true, }; -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: 'Webhook', diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue index 6d0492250d..a978be0ae5 100644 --- a/packages/frontend/src/pages/share.vue +++ b/packages/frontend/src/pages/share.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only :renote="renote" :initialVisibleUsers="visibleUsers" class="_panel" - @posted="state = 'posted'" + @posted="onPosted" /> <div v-else-if="state === 'posted'" class="_buttonsCenter"> <MkButton primary @click="close">{{ i18n.ts.close }}</MkButton> @@ -30,43 +30,43 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -// SPECIFICATION: https://misskey-hub.net/docs/features/share-form.html +// SPECIFICATION: https://misskey-hub.net/docs/for-users/features/share-form/ -import { } from 'vue'; +import { ref, computed } from 'vue'; 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 { mainRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { postMessageToParentWindow } from '@/scripts/post-message.js'; import { i18n } from '@/i18n.js'; const urlParams = new URLSearchParams(window.location.search); const localOnlyQuery = urlParams.get('localOnly'); const visibilityQuery = urlParams.get('visibility') as typeof Misskey.noteVisibilities[number]; -let state = $ref('fetching' as 'fetching' | 'writing' | 'posted'); -let title = $ref(urlParams.get('title')); +const state = ref<'fetching' | 'writing' | 'posted'>('fetching'); +const title = ref(urlParams.get('title')); const text = urlParams.get('text'); const url = urlParams.get('url'); -let initialText = $ref<string | undefined>(); -let reply = $ref<Misskey.entities.Note | undefined>(); -let renote = $ref<Misskey.entities.Note | undefined>(); -let visibility = $ref(Misskey.noteVisibilities.includes(visibilityQuery) ? visibilityQuery : undefined); -let localOnly = $ref(localOnlyQuery === '0' ? false : localOnlyQuery === '1' ? true : undefined); -let files = $ref([] as Misskey.entities.DriveFile[]); -let visibleUsers = $ref([] as Misskey.entities.User[]); +const initialText = ref<string | undefined>(); +const reply = ref<Misskey.entities.Note | undefined>(); +const renote = ref<Misskey.entities.Note | undefined>(); +const visibility = ref(Misskey.noteVisibilities.includes(visibilityQuery) ? visibilityQuery : undefined); +const localOnly = ref(localOnlyQuery === '0' ? false : localOnlyQuery === '1' ? true : undefined); +const files = ref([] as Misskey.entities.DriveFile[]); +const visibleUsers = ref([] as Misskey.entities.User[]); async function init() { let noteText = ''; - if (title) noteText += `[ ${title} ]\n`; + if (title.value) noteText += `[ ${title.value} ]\n`; // Googleニュース対策 - if (text?.startsWith(`${title}.\n`)) noteText += text.replace(`${title}.\n`, ''); - else if (text && title !== text) noteText += `${text}\n`; + if (text?.startsWith(`${title.value}.\n`)) noteText += text.replace(`${title.value}.\n`, ''); + else if (text && title.value !== text) noteText += `${text}\n`; if (url) noteText += `${url}`; - initialText = noteText.trim(); + initialText.value = noteText.trim(); - if (visibility === 'specified') { + if (visibility.value === 'specified') { const visibleUserIds = urlParams.get('visibleUserIds'); const visibleAccts = urlParams.get('visibleAccts'); await Promise.all( @@ -78,7 +78,7 @@ async function init() { .map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q) .map(q => os.api('users/show', q) .then(user => { - visibleUsers.push(user); + visibleUsers.value.push(user); }, () => { console.error(`Invalid user query: ${JSON.stringify(q)}`); }), @@ -91,7 +91,7 @@ async function init() { const replyId = urlParams.get('replyId'); const replyUri = urlParams.get('replyUri'); if (replyId) { - reply = await os.api('notes/show', { + reply.value = await os.api('notes/show', { noteId: replyId, }); } else if (replyUri) { @@ -99,7 +99,7 @@ async function init() { uri: replyUri, }); if (obj.type === 'Note') { - reply = obj.object; + reply.value = obj.object; } } //#endregion @@ -108,7 +108,7 @@ async function init() { const renoteId = urlParams.get('renoteId'); const renoteUri = urlParams.get('renoteUri'); if (renoteId) { - renote = await os.api('notes/show', { + renote.value = await os.api('notes/show', { noteId: renoteId, }); } else if (renoteUri) { @@ -116,7 +116,7 @@ async function init() { uri: renoteUri, }); if (obj.type === 'Note') { - renote = obj.object; + renote.value = obj.object; } } //#endregion @@ -128,7 +128,7 @@ async function init() { fileIds.split(',') .map(fileId => os.api('drive/files/show', { fileId }) .then(file => { - files.push(file); + files.value.push(file); }, () => { console.error(`Failed to fetch a file ${fileId}`); }), @@ -144,7 +144,7 @@ async function init() { }); } - state = 'writing'; + state.value = 'writing'; } init(); @@ -162,9 +162,14 @@ function goToMisskey(): void { location.href = '/'; } -const headerActions = $computed(() => []); +function onPosted(): void { + state.value = 'posted'; + postMessageToParentWindow('misskey:shareForm:shareCompleted'); +} + +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.share, diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue index 0bb6548447..4009652bcf 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -25,22 +25,22 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkAnimBg from '@/components/MkAnimBg.vue'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -let submitting = $ref(false); +const submitting = ref(false); const props = defineProps<{ code: string; }>(); function submit() { - if (submitting) return; - submitting = true; + if (submitting.value) return; + submitting.value = true; os.api('signup-pending', { code: props.code, @@ -54,7 +54,7 @@ function submit() { } return login(res.i, '/'); }).catch(() => { - submitting = false; + submitting.value = false; os.alert({ type: 'error', diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index 7ac485685e..167816638c 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -51,9 +51,9 @@ async function post() { notes.value?.pagingComponent?.reload(); } -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(computed(() => ({ title: props.tag, diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index 1bc5eaddd9..6c4a54eb5c 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -51,9 +51,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.editCode }}</template> <div class="_gaps_m"> - <MkTextarea v-model="themeCode" tall> + <MkCodeEditor v-model="themeCode" lang="json5"> <template #label>{{ i18n.ts._theme.code }}</template> - </MkTextarea> + </MkCodeEditor> <MkButton primary @click="applyThemeCode">{{ i18n.ts.apply }}</MkButton> </div> </MkFolder> @@ -73,13 +73,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, ref, computed } from 'vue'; import { toUnicode } from 'punycode/'; import tinycolor from 'tinycolor2'; import { v4 as uuid } from 'uuid'; import JSON5 from 'json5'; import MkButton from '@/components/MkButton.vue'; +import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkFolder from '@/components/MkFolder.vue'; @@ -124,53 +125,53 @@ const fgColors = [ { color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' }, ]; -let theme = $ref<Partial<Theme>>({ +const theme = ref<Partial<Theme>>({ base: 'light', props: lightTheme.props, }); -let description = $ref<string | null>(null); -let themeCode = $ref<string | null>(null); -let changed = $ref(false); +const description = ref<string | null>(null); +const themeCode = ref<string | null>(null); +const changed = ref(false); -useLeaveGuard($$(changed)); +useLeaveGuard(changed); function setBgColor(color: typeof bgColors[number]) { - if (theme.base !== color.kind) { + if (theme.value.base !== color.kind) { const base = color.kind === 'dark' ? darkTheme : lightTheme; for (const prop of Object.keys(base.props)) { if (prop === 'accent') continue; if (prop === 'fg') continue; - theme.props[prop] = base.props[prop]; + theme.value.props[prop] = base.props[prop]; } } - theme.base = color.kind; - theme.props.bg = color.color; + theme.value.base = color.kind; + theme.value.props.bg = color.color; - if (theme.props.fg) { - const matchedFgColor = fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(theme.props.fg).toRgbString())); + if (theme.value.props.fg) { + const matchedFgColor = fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(theme.value.props.fg).toRgbString())); if (matchedFgColor) setFgColor(matchedFgColor); } } function setAccentColor(color) { - theme.props.accent = color; + theme.value.props.accent = color; } function setFgColor(color) { - theme.props.fg = theme.base === 'light' ? color.forLight : color.forDark; + theme.value.props.fg = theme.value.base === 'light' ? color.forLight : color.forDark; } function apply() { - themeCode = JSON5.stringify(theme, null, '\t'); - applyTheme(theme, false); - changed = true; + themeCode.value = JSON5.stringify(theme.value, null, '\t'); + applyTheme(theme.value, false); + changed.value = true; } function applyThemeCode() { let parsed; try { - parsed = JSON5.parse(themeCode); + parsed = JSON5.parse(themeCode.value); } catch (err) { os.alert({ type: 'error', @@ -179,7 +180,7 @@ function applyThemeCode() { return; } - theme = parsed; + theme.value = parsed; } async function saveAs() { @@ -189,34 +190,34 @@ async function saveAs() { }); if (canceled) return; - theme.id = uuid(); - theme.name = name; - theme.author = `@${$i.username}@${toUnicode(host)}`; - if (description) theme.desc = description; - await addTheme(theme); - applyTheme(theme); + theme.value.id = uuid(); + theme.value.name = name; + theme.value.author = `@${$i.username}@${toUnicode(host)}`; + if (description.value) theme.value.desc = description.value; + await addTheme(theme.value); + applyTheme(theme.value); if (defaultStore.state.darkMode) { - ColdDeviceStorage.set('darkTheme', theme); + ColdDeviceStorage.set('darkTheme', theme.value); } else { - ColdDeviceStorage.set('lightTheme', theme); + ColdDeviceStorage.set('lightTheme', theme.value); } - changed = false; + changed.value = false; os.alert({ type: 'success', - text: i18n.t('_theme.installed', { name: theme.name }), + text: i18n.t('_theme.installed', { name: theme.value.name }), }); } -watch($$(theme), apply, { deep: true }); +watch(theme, apply, { deep: true }); -const headerActions = $computed(() => [{ +const headerActions = computed(() => [{ asFullButton: true, icon: 'ph-check ph-bold ph-lg', text: i18n.ts.saveAs, handler: saveAs, }]); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata({ title: i18n.ts.themeEditor, diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 2f63ec9a38..f9adee94dc 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, provide } from 'vue'; +import { computed, watch, provide, shallowRef, ref } from 'vue'; import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -46,9 +46,10 @@ import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import { $i } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { miLocalStorage } from '@/local-storage.js'; import { antennasCache, userListsCache } from '@/cache.js'; import { deviceKind } from '@/scripts/device-kind.js'; +import { MenuItem } from '@/types/menu.js'; +import { miLocalStorage } from '@/local-storage.js'; provide('shouldOmitHeaderTitle', true); @@ -59,49 +60,67 @@ const keymap = { 't': focus, }; -const tlComponent = $shallowRef<InstanceType<typeof MkTimeline>>(); -const rootEl = $shallowRef<HTMLElement>(); +const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>(); +const rootEl = shallowRef<HTMLElement>(); -let queue = $ref(0); -let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global'); -const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) }); -const withRenotes = $ref(true); -const withReplies = $ref($i ? defaultStore.state.tlWithReplies : false); -const withBots = $ref($i ? defaultStore.state.tlWithBots : true); -const onlyFiles = $ref(false); +const queue = ref(0); +const srcWhenNotSignin = ref(isLocalTimelineAvailable ? 'local' : 'global'); +const src = computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value), set: (x) => saveSrc(x) }); +const withRenotes = ref(true); +const withReplies = ref($i ? defaultStore.state.tlWithReplies : false); +const withBots = ref($i ? defaultStore.state.tlWithBots : true); +const onlyFiles = ref(false); -watch($$(src), () => queue = 0); +watch(src, () => queue.value = 0); -watch($$(withReplies), (x) => { +watch(withReplies, (x) => { if ($i) defaultStore.set('tlWithReplies', x); }); function queueUpdated(q: number): void { - queue = q; + queue.value = q; } function top(): void { - if (rootEl) scroll(rootEl, { top: 0 }); + if (rootEl.value) scroll(rootEl.value, { top: 0 }); } async function chooseList(ev: MouseEvent): Promise<void> { const lists = await userListsCache.fetch(); - const items = lists.map(list => ({ - type: 'link' as const, - text: list.name, - to: `/timeline/list/${list.id}`, - })); + const items: MenuItem[] = [ + ...lists.map(list => ({ + type: 'link' as const, + text: list.name, + to: `/timeline/list/${list.id}`, + })), + (lists.length === 0 ? undefined : { type: 'divider' }), + { + type: 'link' as const, + icon: 'ti ti-plus', + text: i18n.ts.createNew, + to: '/my/lists', + }, + ]; os.popupMenu(items, ev.currentTarget ?? ev.target); } async function chooseAntenna(ev: MouseEvent): Promise<void> { const antennas = await antennasCache.fetch(); - const items = antennas.map(antenna => ({ - type: 'link' as const, - text: antenna.name, - indicate: antenna.hasUnreadNote, - to: `/timeline/antenna/${antenna.id}`, - })); + const items: MenuItem[] = [ + ...antennas.map(antenna => ({ + type: 'link' as const, + text: antenna.name, + indicate: antenna.hasUnreadNote, + to: `/timeline/antenna/${antenna.id}`, + })), + (antennas.length === 0 ? undefined : { type: 'divider' }), + { + type: 'link' as const, + icon: 'ti ti-plus', + text: i18n.ts.createNew, + to: '/my/antennas', + }, + ]; os.popupMenu(items, ev.currentTarget ?? ev.target); } @@ -109,16 +128,30 @@ async function chooseChannel(ev: MouseEvent): Promise<void> { const channels = await os.api('channels/my-favorites', { limit: 100, }); - const items = channels.map(channel => ({ - type: 'link' as const, - text: channel.name, - indicate: channel.hasUnreadNote, - to: `/channels/${channel.id}`, - })); + const items: MenuItem[] = [ + ...channels.map(channel => { + const lastReadedAt = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.id}`) ?? null; + const hasUnreadNote = (lastReadedAt && channel.lastNotedAt) ? Date.parse(channel.lastNotedAt) > lastReadedAt : !!(!lastReadedAt && channel.lastNotedAt); + + return { + type: 'link' as const, + text: channel.name, + indicate: hasUnreadNote, + to: `/channels/${channel.id}`, + }; + }), + (channels.length === 0 ? undefined : { type: 'divider' }), + { + type: 'link' as const, + icon: 'ti ti-plus', + text: i18n.ts.createNew, + to: '/channels', + }, + ]; os.popupMenu(items, ev.currentTarget ?? ev.target); } -function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void { +function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | 'bubble' | `list:${string}`): void { let userList = null; if (newSrc.startsWith('userList:')) { const id = newSrc.substring('userList:'.length); @@ -128,7 +161,7 @@ function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string src: newSrc, userList, }); - srcWhenNotSignin = newSrc; + srcWhenNotSignin.value = newSrc; } async function timetravel(): Promise<void> { @@ -137,21 +170,21 @@ async function timetravel(): Promise<void> { }); if (canceled) return; - tlComponent.timetravel(date); + tlComponent.value.timetravel(date); } function focus(): void { - tlComponent.focus(); + tlComponent.value.focus(); } function closeTutorial(): void { - if (!['home', 'local', 'social', 'global'].includes(src)) return; + if (!['home', 'local', 'social', 'global'].includes(src.value)) return; const before = defaultStore.state.timelineTutorials; - before[src] = true; + before[src.value] = true; defaultStore.set('timelineTutorials', before); } -const headerActions = $computed(() => { +const headerActions = computed(() => { const tmp = [ { icon: 'ph-dots-three ph-bold ph-lg', @@ -160,17 +193,17 @@ const headerActions = $computed(() => { os.popupMenu([{ type: 'switch', text: i18n.ts.showRenotes, - icon: 'ph-rocket-launch ph-bold ph-lg', - ref: $$(withRenotes), - }, src === 'local' || src === 'social' ? { + ref: withRenotes, + }, src.value === 'local' || src.value === 'social' ? { type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, - ref: $$(withReplies), + ref: withReplies, + disabled: onlyFiles, } : undefined, { type: 'switch', text: i18n.ts.fileAttachedOnly, - icon: 'ph-image ph-bold ph-lg', - ref: $$(onlyFiles), + ref: onlyFiles, + disabled: src.value === 'local' || src.value === 'social' ? withReplies : false, }], ev.currentTarget ?? ev.target); }, }, @@ -181,14 +214,14 @@ const headerActions = $computed(() => { text: i18n.ts.reload, handler: (ev: Event) => { console.log('called'); - tlComponent.reloadTimeline(); + tlComponent.value.reloadTimeline(); }, }); } return tmp; }); -const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({ +const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({ key: 'list:' + l.id, title: l.name, icon: 'ph-star ph-bold ph-lg', @@ -235,7 +268,7 @@ const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLis onClick: chooseChannel, }] as Tab[]); -const headerTabsWhenNotLogin = $computed(() => [ +const headerTabsWhenNotLogin = computed(() => [ ...(isLocalTimelineAvailable ? [{ key: 'local', title: i18n.ts._timelines.local, @@ -252,7 +285,7 @@ const headerTabsWhenNotLogin = $computed(() => [ definePageMetadata(computed(() => ({ title: i18n.ts.timeline, - icon: src === 'local' ? 'ph-planet ph-bold ph-lg' : src === 'social' ? 'ph-rocket-launch ph-bold ph-lg' : src === 'global' ? 'ph-globe-hemisphere-west ph-bold ph-lg' : 'ph-house ph-bold ph-lg', + icon: src.value === 'local' ? 'ph-planet ph-bold ph-lg' : src.value === 'social' ? 'ph-rocket-launch ph-bold ph-lg' : src.value === 'global' ? 'ph-globe-hemisphere-west ph-bold ph-lg' : src.value === 'bubble' ? 'ph-drop ph-bold ph-lg' : 'ph-house ph-bold ph-lg', }))); </script> diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index e1cb7997e1..6d203a2882 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref, shallowRef } from 'vue'; import MkTimeline from '@/components/MkTimeline.vue'; import { scroll } from '@/scripts/scroll.js'; import * as os from '@/os.js'; @@ -38,39 +38,39 @@ const props = defineProps<{ listId: string; }>(); -let list = $ref(null); -let queue = $ref(0); -let tlEl = $shallowRef<InstanceType<typeof MkTimeline>>(); -let rootEl = $shallowRef<HTMLElement>(); +const list = ref(null); +const queue = ref(0); +const tlEl = shallowRef<InstanceType<typeof MkTimeline>>(); +const rootEl = shallowRef<HTMLElement>(); watch(() => props.listId, async () => { - list = await os.api('users/lists/show', { + list.value = await os.api('users/lists/show', { listId: props.listId, }); }, { immediate: true }); function queueUpdated(q) { - queue = q; + queue.value = q; } function top() { - scroll(rootEl, { top: 0 }); + scroll(rootEl.value, { top: 0 }); } function settings() { router.push(`/my/lists/${props.listId}`); } -const headerActions = $computed(() => list ? [{ +const headerActions = computed(() => list.value ? [{ icon: 'ph-gear ph-bold ph-lg', text: i18n.ts.settings, handler: settings, }] : []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(computed(() => list ? { - title: list.name, +definePageMetadata(computed(() => list.value ? { + title: list.value.name, icon: 'ph-list ph-bold ph-lg', } : null)); </script> diff --git a/packages/frontend/src/pages/user-tag.vue b/packages/frontend/src/pages/user-tag.vue index 21ad7e7e5e..7e6757bba5 100644 --- a/packages/frontend/src/pages/user-tag.vue +++ b/packages/frontend/src/pages/user-tag.vue @@ -16,8 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; -import * as os from '@/os.js'; +import { computed } from 'vue'; import MkUserList from '@/components/MkUserList.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -25,7 +24,7 @@ const props = defineProps<{ tag: string; }>(); -const tagUsers = $computed(() => ({ +const tagUsers = computed(() => ({ endpoint: 'hashtags/users' as const, limit: 30, params: { diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue index bc6bf77168..bd1159cb32 100644 --- a/packages/frontend/src/pages/user/activity.following.vue +++ b/packages/frontend/src/pages/user/activity.following.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, shallowRef, ref } from 'vue'; import { Chart, ChartDataset } from 'chart.js'; import * as Misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; @@ -32,12 +32,12 @@ const props = defineProps<{ user: Misskey.entities.User; }>(); -const chartEl = $shallowRef<HTMLCanvasElement>(null); -let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); +const chartEl = shallowRef<HTMLCanvasElement>(null); +const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>(); const now = new Date(); let chartInstance: Chart = null; const chartLimit = 30; -let fetching = $ref(true); +const fetching = ref(true); const { handler: externalTooltipHandler } = useChartTooltip(); @@ -88,7 +88,7 @@ async function renderChart() { }, extra); } - chartInstance = new Chart(chartEl, { + chartInstance = new Chart(chartEl.value, { type: 'bar', data: { datasets: [ @@ -162,10 +162,10 @@ async function renderChart() { gradient, }, }, - plugins: [chartVLine(vLineColor), chartLegend(legendEl)], + plugins: [chartVLine(vLineColor), chartLegend(legendEl.value)], }); - fetching = false; + fetching.value = false; } onMounted(async () => { diff --git a/packages/frontend/src/pages/user/activity.heatmap.vue b/packages/frontend/src/pages/user/activity.heatmap.vue index 73bec2487e..ff46db9653 100644 --- a/packages/frontend/src/pages/user/activity.heatmap.vue +++ b/packages/frontend/src/pages/user/activity.heatmap.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, nextTick, watch } from 'vue'; +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'; @@ -29,11 +29,11 @@ const props = defineProps<{ user: Misskey.entities.User; }>(); -const rootEl = $shallowRef<HTMLDivElement>(null); -const chartEl = $shallowRef<HTMLCanvasElement>(null); +const rootEl = shallowRef<HTMLDivElement>(null); +const chartEl = shallowRef<HTMLCanvasElement>(null); const now = new Date(); let chartInstance: Chart = null; -let fetching = $ref(true); +const fetching = ref(true); const { handler: externalTooltipHandler } = useChartTooltip({ position: 'middle', @@ -44,8 +44,8 @@ async function renderChart() { chartInstance.destroy(); } - const wide = rootEl.offsetWidth > 700; - const narrow = rootEl.offsetWidth < 400; + const wide = rootEl.value.offsetWidth > 700; + const narrow = rootEl.value.offsetWidth < 400; const weeks = wide ? 50 : narrow ? 10 : 25; const chartLimit = 7 * weeks; @@ -78,7 +78,7 @@ async function renderChart() { values = raw.inc; } - fetching = false; + fetching.value = false; await nextTick(); @@ -91,7 +91,7 @@ async function renderChart() { const marginEachCell = 4; - chartInstance = new Chart(chartEl, { + chartInstance = new Chart(chartEl.value, { type: 'matrix', data: { datasets: [{ @@ -203,7 +203,7 @@ async function renderChart() { } watch(() => props.src, () => { - fetching = true; + fetching.value = true; renderChart(); }); diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue index 5ba4af7ca1..dd035641d8 100644 --- a/packages/frontend/src/pages/user/activity.notes.vue +++ b/packages/frontend/src/pages/user/activity.notes.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, shallowRef, ref } from 'vue'; import { Chart, ChartDataset } from 'chart.js'; import * as Misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; @@ -32,12 +32,12 @@ const props = defineProps<{ user: Misskey.entities.User; }>(); -const chartEl = $shallowRef<HTMLCanvasElement>(null); -let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); +const chartEl = shallowRef<HTMLCanvasElement>(null); +const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>(); const now = new Date(); let chartInstance: Chart = null; const chartLimit = 50; -let fetching = $ref(true); +const fetching = ref(true); const { handler: externalTooltipHandler } = useChartTooltip(); @@ -87,7 +87,7 @@ async function renderChart() { }, extra); } - chartInstance = new Chart(chartEl, { + chartInstance = new Chart(chartEl.value, { type: 'bar', data: { datasets: [ @@ -161,10 +161,10 @@ async function renderChart() { gradient, }, }, - plugins: [chartVLine(vLineColor), chartLegend(legendEl)], + plugins: [chartVLine(vLineColor), chartLegend(legendEl.value)], }); - fetching = false; + fetching.value = false; } onMounted(async () => { diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue index 54d1d0c1be..2dd9a1570f 100644 --- a/packages/frontend/src/pages/user/activity.pv.vue +++ b/packages/frontend/src/pages/user/activity.pv.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, shallowRef, ref } from 'vue'; import { Chart, ChartDataset } from 'chart.js'; import * as Misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; @@ -32,12 +32,12 @@ const props = defineProps<{ user: Misskey.entities.User; }>(); -const chartEl = $shallowRef<HTMLCanvasElement>(null); -let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); +const chartEl = shallowRef<HTMLCanvasElement>(null); +const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>(); const now = new Date(); let chartInstance: Chart = null; const chartLimit = 30; -let fetching = $ref(true); +const fetching = ref(true); const { handler: externalTooltipHandler } = useChartTooltip(); @@ -88,7 +88,7 @@ async function renderChart() { }, extra); } - chartInstance = new Chart(chartEl, { + chartInstance = new Chart(chartEl.value, { type: 'bar', data: { datasets: [ @@ -172,10 +172,10 @@ async function renderChart() { gradient, }, }, - plugins: [chartVLine(vLineColor), chartLegend(legendEl)], + plugins: [chartVLine(vLineColor), chartLegend(legendEl.value)], }); - fetching = false; + fetching.value = false; } onMounted(async () => { diff --git a/packages/frontend/src/pages/user/followers.vue b/packages/frontend/src/pages/user/followers.vue index b41edc74a1..5da950e853 100644 --- a/packages/frontend/src/pages/user/followers.vue +++ b/packages/frontend/src/pages/user/followers.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XFollowList from './follow-list.vue'; import * as os from '@/os.js'; @@ -31,16 +31,16 @@ const props = withDefaults(defineProps<{ }>(), { }); -let user = $ref<null | Misskey.entities.UserDetailed>(null); -let error = $ref(null); +const user = ref<null | Misskey.entities.UserDetailed>(null); +const error = ref(null); function fetchUser(): void { if (props.acct == null) return; - user = null; + user.value = null; os.api('users/show', Misskey.acct.parse(props.acct)).then(u => { - user = u; + user.value = u; }).catch(err => { - error = err; + error.value = err; }); } @@ -48,15 +48,15 @@ watch(() => props.acct, fetchUser, { immediate: true, }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(computed(() => user ? { +definePageMetadata(computed(() => user.value ? { icon: 'ph-user ph-bold ph-lg', - title: user.name ? `${user.name} (@${user.username})` : `@${user.username}`, + title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`, subtitle: i18n.ts.followers, - userName: user, - avatar: user, + userName: user.value, + avatar: user.value, } : null)); </script> diff --git a/packages/frontend/src/pages/user/following.vue b/packages/frontend/src/pages/user/following.vue index ba5ddafbdb..6ba134cd78 100644 --- a/packages/frontend/src/pages/user/following.vue +++ b/packages/frontend/src/pages/user/following.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XFollowList from './follow-list.vue'; import * as os from '@/os.js'; @@ -31,16 +31,16 @@ const props = withDefaults(defineProps<{ }>(), { }); -let user = $ref<null | Misskey.entities.UserDetailed>(null); -let error = $ref(null); +const user = ref<null | Misskey.entities.UserDetailed>(null); +const error = ref(null); function fetchUser(): void { if (props.acct == null) return; - user = null; + user.value = null; os.api('users/show', Misskey.acct.parse(props.acct)).then(u => { - user = u; + user.value = u; }).catch(err => { - error = err; + error.value = err; }); } @@ -48,15 +48,15 @@ watch(() => props.acct, fetchUser, { immediate: true, }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); -definePageMetadata(computed(() => user ? { +definePageMetadata(computed(() => user.value ? { icon: 'ph-user ph-bold ph-lg', - title: user.name ? `${user.name} (@${user.username})` : `@${user.username}`, + title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`, subtitle: i18n.ts.following, - userName: user, - avatar: user, + userName: user.value, + avatar: user.value, } : null)); </script> diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 2e7fc68748..44a8ca250b 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -107,11 +107,11 @@ SPDX-License-Identifier: AGPL-3.0-only <b>{{ number(user.notesCount) }}</b> <span>{{ i18n.ts.notes }}</span> </MkA> - <MkA v-if="isFfVisibleForMe(user)" :to="userPage(user, 'following')"> + <MkA v-if="isFollowingVisibleForMe(user)" :to="userPage(user, 'following')"> <b>{{ number(user.followingCount) }}</b> <span>{{ i18n.ts.following }}</span> </MkA> - <MkA v-if="isFfVisibleForMe(user)" :to="userPage(user, 'followers')"> + <MkA v-if="isFollowersVisibleForMe(user)" :to="userPage(user, 'followers')"> <b>{{ number(user.followersCount) }}</b> <span>{{ i18n.ts.followers }}</span> </MkA> @@ -128,13 +128,20 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo> <template v-if="narrow"> - <XFiles :key="user.id" :user="user"/> - <XActivity :key="user.id" :user="user"/> - <XListenBrainz v-if="user.listenbrainz && listenbrainzdata" :key="user.id" :user="user"/> + <MkLazy> + <XFiles :key="user.id" :user="user"/> + </MkLazy> + <MkLazy> + <XActivity :key="user.id" :user="user"/> + </MkLazy> + <MkLazy> + <XListenBrainz v-if="user.listenbrainz && listenbrainzdata" :key="user.id" :user="user"/> + </MkLazy> </template> <!-- <div v-if="!disableNotes"> - <div style="margin-bottom: 8px; z-index: 1;">{{ i18n.ts.featured }}</div> - <MkNotes :class="$style.tl" :noGap="true" :pagination="pagination"/> + <MkLazy> + <XTimeline :user="user"/> + </MkLazy> </div> --> <MkStickyContainer> <template #header> @@ -144,7 +151,9 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="files">{{ i18n.ts.withFiles }}</option> </MkTab> </template> - <MkNotes :class="$style.tl" :noGap="true" :pagination="AllPagination"/> + <MkLazy> + <MkNotes :class="$style.tl" :noGap="true" :pagination="AllPagination"/> + </MkLazy> </MkStickyContainer> </div> </div> @@ -159,10 +168,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'; +import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkTab from '@/components/MkTab.vue'; import MkNote from '@/components/MkNote.vue'; +import MkNotes from '@/components/MkNotes.vue'; import SkNote from '@/components/SkNote.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; import MkAccountMoved from '@/components/MkAccountMoved.vue'; @@ -180,10 +190,9 @@ import { i18n } from '@/i18n.js'; import { $i, iAmModerator } from '@/account.js'; import { dateString } from '@/filters/date.js'; import { confetti } from '@/scripts/confetti.js'; -import MkNotes from '@/components/MkNotes.vue'; import { api } from '@/os.js'; import { defaultStore } from '@/store.js'; -import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; +import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; function calcAge(birthdate: string): number { const date = new Date(birthdate); @@ -203,6 +212,7 @@ function calcAge(birthdate: string): number { const XFiles = defineAsyncComponent(() => import('./index.files.vue')); const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); const XListenBrainz = defineAsyncComponent(() => import("./index.listenbrainz.vue")); +//const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue')); const props = withDefaults(defineProps<{ user: Misskey.entities.UserDetailed; @@ -214,17 +224,17 @@ const props = withDefaults(defineProps<{ const router = useRouter(); -let user = $ref(props.user); -let parallaxAnimationId = $ref<null | number>(null); -let narrow = $ref<null | boolean>(null); -let rootEl = $ref<null | HTMLElement>(null); -let bannerEl = $ref<null | HTMLElement>(null); -let memoTextareaEl = $ref<null | HTMLElement>(null); -let memoDraft = $ref(props.user.memo); -let isEditingMemo = $ref(false); -let moderationNote = $ref(props.user.moderationNote); -let editModerationNote = $ref(false); -let noteview = $ref<string | null>(null); +const user = ref(props.user); +const parallaxAnimationId = ref<null | number>(null); +const narrow = ref<null | boolean>(null); +const rootEl = ref<null | HTMLElement>(null); +const bannerEl = ref<null | HTMLElement>(null); +const memoTextareaEl = ref<null | HTMLElement>(null); +const memoDraft = ref(props.user.memo); +const isEditingMemo = ref(false); +const moderationNote = ref(props.user.moderationNote); +const editModerationNote = ref(false); +const noteview = ref<string | null>(null); let listenbrainzdata = false; if (props.user.listenbrainz) { @@ -251,8 +261,8 @@ const background = computed(() => { }; }); -watch($$(moderationNote), async () => { - await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote }); +watch(moderationNote, async () => { + await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote.value }); }); const pagination = { @@ -268,39 +278,39 @@ const AllPagination = { limit: 10, params: computed(() => ({ userId: props.user.id, - withRenotes: noteview === 'all', - withReplies: noteview === 'all' || noteview === 'files', - withChannelNotes: noteview === 'all', - withFiles: noteview === 'files', + withRenotes: noteview.value === 'all', + withReplies: noteview.value === 'all' || noteview.value === 'files', + withChannelNotes: noteview.value === 'all', + withFiles: noteview.value === 'files', })), }; -const style = $computed(() => { +const style = computed(() => { if (props.user.bannerUrl == null) return {}; return { backgroundImage: `url(${ props.user.bannerUrl })`, }; }); -const age = $computed(() => { +const age = computed(() => { return calcAge(props.user.birthday); }); -function menu(ev) { - const { menu, cleanup } = getUserMenu(user, router); +function menu(ev: MouseEvent) { + const { menu, cleanup } = getUserMenu(user.value, router); os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup); } function parallaxLoop() { - parallaxAnimationId = window.requestAnimationFrame(parallaxLoop); + parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop); parallax(); } function parallax() { - const banner = bannerEl as any; + const banner = bannerEl.value as any; if (banner == null) return; - const top = getScrollPosition(rootEl); + const top = getScrollPosition(rootEl.value); if (top < 0) return; @@ -310,33 +320,33 @@ function parallax() { } function showMemoTextarea() { - isEditingMemo = true; + isEditingMemo.value = true; nextTick(() => { - memoTextareaEl?.focus(); + memoTextareaEl.value?.focus(); }); } function adjustMemoTextarea() { - if (!memoTextareaEl) return; - memoTextareaEl.style.height = '0px'; - memoTextareaEl.style.height = `${memoTextareaEl.scrollHeight}px`; + if (!memoTextareaEl.value) return; + memoTextareaEl.value.style.height = '0px'; + memoTextareaEl.value.style.height = `${memoTextareaEl.value.scrollHeight}px`; } async function updateMemo() { await api('users/update-memo', { - memo: memoDraft, + memo: memoDraft.value, userId: props.user.id, }); - isEditingMemo = false; + isEditingMemo.value = false; } watch([props.user], () => { - memoDraft = props.user.memo; + memoDraft.value = props.user.memo; }); onMounted(() => { window.requestAnimationFrame(parallaxLoop); - narrow = rootEl!.clientWidth < 1000; + narrow.value = rootEl.value!.clientWidth < 1000; if (props.user.birthday) { const m = new Date().getMonth() + 1; @@ -355,8 +365,8 @@ onMounted(() => { }); onUnmounted(() => { - if (parallaxAnimationId) { - window.cancelAnimationFrame(parallaxAnimationId); + if (parallaxAnimationId.value) { + window.cancelAnimationFrame(parallaxAnimationId.value); } }); </script> diff --git a/packages/frontend/src/pages/user/index.activity.vue b/packages/frontend/src/pages/user/index.activity.vue index b7576d4e20..f555486a6d 100644 --- a/packages/frontend/src/pages/user/index.activity.vue +++ b/packages/frontend/src/pages/user/index.activity.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkContainer from '@/components/MkContainer.vue'; import MkChart from '@/components/MkChart.vue'; @@ -34,20 +34,20 @@ const props = withDefaults(defineProps<{ limit: 50, }); -let chartSrc = $ref('per-user-notes'); +const chartSrc = ref('per-user-notes'); function showMenu(ev: MouseEvent) { os.popupMenu([{ text: i18n.ts.notes, - active: chartSrc === 'per-user-notes', + active: chartSrc.value === 'per-user-notes', action: () => { - chartSrc = 'per-user-notes'; + chartSrc.value = 'per-user-notes'; }, }, { text: i18n.ts.numberOfProfileView, - active: chartSrc === 'per-user-pv', + active: chartSrc.value === 'per-user-pv', action: () => { - chartSrc = 'per-user-pv'; + chartSrc.value = 'per-user-pv'; }, }, /*, { text: i18n.ts.following, diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue index d6d90c46e0..30817db77c 100644 --- a/packages/frontend/src/pages/user/index.files.vue +++ b/packages/frontend/src/pages/user/index.files.vue @@ -11,10 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLoading v-if="fetching"/> <div v-if="!fetching && files.length > 0" :class="$style.stream"> <template v-for="file in files" :key="file.note.id + file.file.id"> - <div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.sensitive" @click="showingFiles.push(file.file.id)"> - <div> - <div><i class="ph-eye-slash ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}</div> - <div>{{ i18n.ts.clickToShow }}</div> + <div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.img" @click="showingFiles.push(file.file.id)"> + <!-- TODO: 画像以外のファイルに対応 --> + <ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/> + <div :class="$style.sensitive"> + <div> + <div><i class="ph-eye-slash ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}</div> + <div>{{ i18n.ts.clickToShow }}</div> + </div> </div> </div> <MkA v-else :class="$style.img" :to="notePage(file.note)"> @@ -29,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { notePage } from '@/filters/note.js'; @@ -43,12 +47,12 @@ const props = defineProps<{ user: Misskey.entities.UserDetailed; }>(); -let fetching = $ref(true); -let files = $ref<{ +const fetching = ref(true); +const files = ref<{ note: Misskey.entities.Note; file: Misskey.entities.DriveFile; }[]>([]); -let showingFiles = $ref<string[]>([]); +const showingFiles = ref<string[]>([]); function thumbnail(image: Misskey.entities.DriveFile): string { return defaultStore.state.disableShowingAnimatedImages @@ -60,18 +64,17 @@ onMounted(() => { os.api('users/notes', { userId: props.user.id, withFiles: true, - excludeNsfw: defaultStore.state.nsfw !== 'ignore', limit: 15, }).then(notes => { for (const note of notes) { for (const file of note.files) { - files.push({ + files.value.push({ note, file, }); } } - fetching = false; + fetching.value = false; }); }); </script> @@ -88,6 +91,7 @@ onMounted(() => { } .img { + position: relative; height: 128px; border-radius: var(--radius-sm); overflow: clip; @@ -99,8 +103,24 @@ onMounted(() => { text-align: center; } +.sensitiveImg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + filter: brightness(0.7); +} .sensitive { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; display: grid; place-items: center; + font-size: 0.8em; + color: #fff; + cursor: pointer; } </style> diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue index 6cf5bcf91f..e5a0f49e3d 100644 --- a/packages/frontend/src/pages/user/index.timeline.vue +++ b/packages/frontend/src/pages/user/index.timeline.vue @@ -4,18 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkSpacer :contentMax="800" style="padding-top: 0"> - <MkStickyContainer> - <template #header> - <MkTab v-model="include" :class="$style.tab"> - <option :value="null">{{ i18n.ts.notes }}</option> - <option value="all">{{ i18n.ts.all }}</option> - <option value="files">{{ i18n.ts.withFiles }}</option> - </MkTab> - </template> - <MkNotes :noGap="true" :pagination="pagination" :class="$style.tl"/> - </MkStickyContainer> -</MkSpacer> +<MkStickyContainer> + <template #header> + <MkTab v-model="tab" :class="$style.tab"> + <option value="featured">{{ i18n.ts.featured }}</option> + <option :value="null">{{ i18n.ts.notes }}</option> + <option value="all">{{ i18n.ts.all }}</option> + <option value="files">{{ i18n.ts.withFiles }}</option> + </MkTab> + </template> + <MkNotes :noGap="true" :pagination="pagination" :class="$style.tl"/> +</MkStickyContainer> </template> <script lang="ts" setup> @@ -29,24 +28,29 @@ const props = defineProps<{ user: Misskey.entities.UserDetailed; }>(); -const include = ref<string | null>('all'); +const tab = ref<string | null>('all'); -const pagination = { +const pagination = computed(() => tab.value === 'featured' ? { + endpoint: 'users/featured-notes' as const, + limit: 10, + params: { + userId: props.user.id, + }, +} : { endpoint: 'users/notes' as const, limit: 10, - params: computed(() => ({ + params: { userId: props.user.id, - withRenotes: include.value === 'all', - withReplies: include.value === 'all', - withChannelNotes: include.value === 'all', - withFiles: include.value === 'files', - })), -}; + withRenotes: tab.value === 'all', + withReplies: tab.value === 'all', + withChannelNotes: tab.value === 'all', + withFiles: tab.value === 'files', + }, +}); </script> <style lang="scss" module> .tab { - margin: calc(var(--margin) / 2) 0; padding: calc(var(--margin) / 2) 0; background: var(--bg); } diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index df07dd9786..c0064d2503 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -9,7 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div> <div v-if="user"> <XHome v-if="tab === 'home'" :user="user"/> - <XTimeline v-else-if="tab === 'notes'" :user="user"/> + <MkSpacer v-else-if="tab === 'notes'" :contentMax="800" style="padding-top: 0"> + <XTimeline :user="user"/> + </MkSpacer> <XActivity v-else-if="tab === 'activity'" :user="user"/> <XAchievements v-else-if="tab === 'achievements'" :user="user"/> <XReactions v-else-if="tab === 'reactions'" :user="user"/> @@ -18,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XPages v-else-if="tab === 'pages'" :user="user"/> <XFlashs v-else-if="tab === 'flashs'" :user="user"/> <XGallery v-else-if="tab === 'gallery'" :user="user"/> + <XRaw v-else-if="tab === 'raw'" :user="user"/> </div> <MkError v-else-if="error" @retry="fetchUser()"/> <MkLoading v-else/> @@ -26,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, computed, watch } from 'vue'; +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'; @@ -44,6 +47,7 @@ const XLists = defineAsyncComponent(() => import('./lists.vue')); const XPages = defineAsyncComponent(() => import('./pages.vue')); const XFlashs = defineAsyncComponent(() => import('./flashs.vue')); const XGallery = defineAsyncComponent(() => import('./gallery.vue')); +const XRaw = defineAsyncComponent(() => import('./raw.vue')); const props = withDefaults(defineProps<{ acct: string; @@ -52,17 +56,17 @@ const props = withDefaults(defineProps<{ page: 'home', }); -let tab = $ref(props.page); -let user = $ref<null | Misskey.entities.UserDetailed>(null); -let error = $ref(null); +const tab = ref(props.page); +const user = ref<null | Misskey.entities.UserDetailed>(null); +const error = ref(null); function fetchUser(): void { if (props.acct == null) return; - user = null; + user.value = null; os.api('users/show', Misskey.acct.parse(props.acct)).then(u => { - user = u; + user.value = u; }).catch(err => { - error = err; + error.value = err; }); } @@ -70,9 +74,9 @@ watch(() => props.acct, fetchUser, { immediate: true, }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => user ? [{ +const headerTabs = computed(() => user.value ? [{ key: 'home', title: i18n.ts.overview, icon: 'ph-house ph-bold ph-lg', @@ -84,11 +88,11 @@ const headerTabs = $computed(() => user ? [{ key: 'activity', title: i18n.ts.activity, icon: 'ph-chart-line ph-bold ph-lg', -}, ...(user.host == null ? [{ +}, ...(user.value.host == null ? [{ key: 'achievements', title: i18n.ts.achievements, icon: 'ph-trophy ph-bold ph-lg', -}] : []), ...($i && ($i.id === user.id)) || user.publicReactions ? [{ +}] : []), ...($i && ($i.id === user.value.id)) || user.value.publicReactions ? [{ key: 'reactions', title: i18n.ts.reaction, icon: 'ph-smiley ph-bold ph-lg', @@ -112,17 +116,21 @@ const headerTabs = $computed(() => user ? [{ key: 'gallery', title: i18n.ts.gallery, icon: 'ph-images-square ph-bold ph-lg', +}, { + key: 'raw', + title: 'Raw', + icon: 'ph-code ph-bold ph-lg', }] : []); -definePageMetadata(computed(() => user ? { +definePageMetadata(computed(() => user.value ? { icon: 'ph-user ph-bold ph-lg', - title: user.name ? `${user.name} (@${user.username})` : `@${user.username}`, - subtitle: `@${getAcct(user)}`, - userName: user, - avatar: user, - path: `/@${user.username}`, + title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`, + subtitle: `@${getAcct(user.value)}`, + userName: user.value, + avatar: user.value, + path: `/@${user.value.username}`, share: { - title: user.name, + title: user.value.name, }, } : null)); </script> diff --git a/packages/frontend/src/pages/user/raw.vue b/packages/frontend/src/pages/user/raw.vue new file mode 100644 index 0000000000..0c0bfc29ca --- /dev/null +++ b/packages/frontend/src/pages/user/raw.vue @@ -0,0 +1,130 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkSpacer :contentMax="600" :marginMin="16" :marginMax="32"> + <div class="_gaps_m"> + <div :class="$style.userMInfoRoot"> + <MkAvatar :class="$style.userMInfoAvatar" :user="user" indicator link preview/> + <div :class="$style.userMInfoMetaRoot"> + <span :class="$style.userMInfoMetaName"><MkUserName :class="$style.userMInfoMetaName" :user="user"/></span> + <span :class="$style.userMInfoMetaSub"><span class="acct _monospace">@{{ acct(user) }}</span></span> + <span :class="$style.userMInfoMetaState"> + <span v-if="suspended" :class="$style.suspended">Suspended</span> + <span v-if="silenced" :class="$style.silenced">Silenced</span> + <span v-if="moderator" :class="$style.moderator">Moderator</span> + </span> + </div> + </div> + + <div style="display: flex; flex-direction: column; gap: 1em;"> + <MkKeyValue :copy="user.id" oneline> + <template #key>ID</template> + <template #value><span class="_monospace">{{ user.id }}</span></template> + </MkKeyValue> + <MkKeyValue oneline> + <template #key>{{ i18n.ts.createdAt }}</template> + <template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template> + </MkKeyValue> + </div> + + <FormSection> + <template #label>Raw</template> + <MkObjectView tall :value="user"></MkObjectView> + </FormSection> + </div> +</MkSpacer> +</template> + +<script lang="ts" setup> +import { computed } from 'vue'; +import * as Misskey from 'misskey-js'; +import { acct } from '@/filters/user.js'; +import { i18n } from '@/i18n.js'; +import MkKeyValue from '@/components/MkKeyValue.vue'; +import FormSection from '@/components/form/section.vue'; +import MkObjectView from '@/components/MkObjectView.vue'; + +const props = defineProps<{ + user: Misskey.entities.User; +}>(); + +const moderator = computed(() => props.user.isModerator ?? false); +const silenced = computed(() => props.user.isSilenced ?? false); +const suspended = computed(() => props.user.isSuspended ?? false); +</script> + +<style lang="scss" module> +.userMInfoRoot { + display: flex; + align-items: center; +} + +.userMInfoAvatar { + display: block; + width: 64px; + height: 64px; + margin-right: 16px; +} + +.userMInfoMetaRoot { + flex: 1; + overflow: hidden; +} + +.userMInfoMetaName { + display: block; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.userMInfoMetaSub { + display: block; + width: 100%; + font-size: 85%; + opacity: 0.7; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.userMInfoMetaState { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 4px; + + &:empty { + display: none; + } + + > .suspended, + > .silenced, + > .moderator { + display: inline-block; + border: solid 1px; + border-radius: 6px; + padding: 2px 6px; + font-size: 85%; + } + + > .suspended { + color: var(--error); + border-color: var(--error); + } + + > .silenced { + color: var(--warn); + border-color: var(--warn); + } + + > .moderator { + color: var(--success); + border-color: var(--success); + } +} +</style> diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue index 52949c294c..50f86a0ae2 100644 --- a/packages/frontend/src/pages/welcome.entrance.a.vue +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XTimeline class="tl"/> <div class="shape1"></div> <div class="shape2"></div> - <img src="/client-assets/sharkey.svg" class="misskey"/> + <img :src="misskeysvg" class="misskey"/> <div class="emojis"> <MkEmoji :normal="true" :noStyle="true" emoji="👍"/> <MkEmoji :normal="true" :noStyle="true" emoji="❤"/> @@ -33,37 +33,35 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import XTimeline from './welcome.timeline.vue'; import MarqueeText from '@/components/MkMarquee.vue'; import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue'; -import MkInfo from '@/components/MkInfo.vue'; -import { instanceName } from '@/config.js'; +import misskeysvg from '/client-assets/sharkey.svg'; import * as os from '@/os.js'; -import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; -import number from '@/filters/number.js'; -import MkNumber from '@/components/MkNumber.vue'; import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue'; import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; -let meta = $ref<Misskey.entities.Instance>(); -let instances = $ref<any[]>(); +const meta = ref<Misskey.entities.MetaResponse>(); +const instances = ref<Misskey.entities.FederationInstance[]>(); -function getInstanceIcon(instance): string { - return getProxiedImageUrl(instance.iconUrl, 'preview'); +function getInstanceIcon(instance: Misskey.entities.FederationInstance): string { + if (!instance.iconUrl) { + return ''; + } + return getProxiedImageUrl(instance.iconUrl, 'preview'); } os.api('meta', { detail: true }).then(_meta => { - meta = _meta; + meta.value = _meta; }); os.apiGet('federation/instances', { sort: '+pubSub', limit: 20, }).then(_instances => { - instances = _instances; + instances.value = _instances; }); </script> diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index bac6cc1f0c..c2f9d4e585 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import { host, version } from '@/config.js'; @@ -44,21 +44,21 @@ import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; import MkAnimBg from '@/components/MkAnimBg.vue'; -let username = $ref(''); -let password = $ref(''); -let submitting = $ref(false); +const username = ref(''); +const password = ref(''); +const submitting = ref(false); function submit() { - if (submitting) return; - submitting = true; + if (submitting.value) return; + submitting.value = true; os.api('admin/accounts/create', { - username: username, - password: password, + username: username.value, + password: password.value, }).then(res => { return login(res.token); }).catch(() => { - submitting = false; + submitting.value = false; os.alert({ type: 'error', diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index 6c13af2f0a..2cbe0ed9b1 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -28,28 +28,27 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { onUpdated } from 'vue'; +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 { getScrollContainer } from '@/scripts/scroll.js'; -import { $i } from '@/account.js'; -let notes = $ref<Misskey.entities.Note[]>([]); -let isScrolling = $ref(false); -let scrollEl = $shallowRef<HTMLElement>(); +const notes = ref<Misskey.entities.Note[]>([]); +const isScrolling = ref(false); +const scrollEl = shallowRef<HTMLElement>(); os.apiGet('notes/featured').then(_notes => { - notes = _notes; + notes.value = _notes; }); onUpdated(() => { - if (!scrollEl) return; - const container = getScrollContainer(scrollEl); + if (!scrollEl.value) return; + const container = getScrollContainer(scrollEl.value); const containerHeight = container ? container.clientHeight : window.innerHeight; - if (scrollEl.offsetHeight > containerHeight) { - isScrolling = true; + if (scrollEl.value.offsetHeight > containerHeight) { + isScrolling.value = true; } }); </script> diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue index 2e6bb8b38f..f7d262cc8a 100644 --- a/packages/frontend/src/pages/welcome.vue +++ b/packages/frontend/src/pages/welcome.vue @@ -11,22 +11,22 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref } from 'vue'; 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 { definePageMetadata } from '@/scripts/page-metadata.js'; -let meta = $ref(null); +const meta = ref(null); os.api('meta', { detail: true }).then(res => { - meta = res; + meta.value = res; }); -const headerActions = $computed(() => []); +const headerActions = computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = computed(() => []); definePageMetadata(computed(() => ({ title: instanceName, diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index e24f646a35..5e49af4858 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -96,7 +96,7 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s }), 'Plugin:open_url': values.FN_NATIVE(([url]) => { utils.assertString(url); - window.open(url.value, '_blank'); + window.open(url.value, '_blank', 'noopener'); }), 'Plugin:config': values.OBJ(config), }; diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index b148325c71..b861afa9a3 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -55,6 +55,10 @@ export const routes = [{ 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')), @@ -63,9 +67,9 @@ export const routes = [{ name: 'privacy', component: page(() => import('./pages/settings/privacy.vue')), }, { - path: '/reaction', - name: 'reaction', - component: page(() => import('./pages/settings/reaction.vue')), + path: '/emoji-picker', + name: 'emojiPicker', + component: page(() => import('./pages/settings/emoji-picker.vue')), }, { path: '/drive', name: 'drive', diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index fb7ab924b7..038ae23109 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -50,6 +50,7 @@ export function createAiScriptEnv(opts) { return values.ERROR('request_failed', utils.jsToVal(err)); }); }), + /* セキュリティ上の問題があるため無効化 'Mk:apiExternal': values.FN_NATIVE(async ([host, ep, param, token]) => { utils.assertString(host); utils.assertString(ep); @@ -60,6 +61,7 @@ export function createAiScriptEnv(opts) { return values.ERROR('request_failed', utils.jsToVal(err)); }); }), + */ 'Mk:save': values.FN_NATIVE(([key, value]) => { utils.assertString(key); miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value))); diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index d326b956e8..75b9248432 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -121,6 +121,7 @@ export type AsUiPostFormButton = AsUiComponentBase & { rounded?: boolean; form?: { text: string; + cw?: string; }; }; @@ -128,6 +129,7 @@ export type AsUiPostForm = AsUiComponentBase & { type: 'postForm'; form?: { text: string; + cw?: string; }; }; @@ -454,8 +456,11 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu const getForm = () => { const text = form!.value.get('text'); utils.assertString(text); + const cw = form!.value.get('cw'); + if (cw) utils.assertString(cw); return { text: text.value, + cw: cw?.value, }; }; @@ -478,8 +483,11 @@ function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn const getForm = () => { const text = form!.value.get('text'); utils.assertString(text); + const cw = form!.value.get('cw'); + if (cw) utils.assertString(cw); return { text: text.value, + cw: cw?.value, }; }; diff --git a/packages/frontend/src/scripts/api.ts b/packages/frontend/src/scripts/api.ts index 080977e5e4..8f3a163938 100644 --- a/packages/frontend/src/scripts/api.ts +++ b/packages/frontend/src/scripts/api.ts @@ -10,7 +10,12 @@ 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']>(endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal): Promise<Misskey.Endpoints[E]['res']> { +export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>( + endpoint: E, + data: P = {} as any, + token?: string | null | undefined, + signal?: AbortSignal, +): Promise<Misskey.api.SwitchCaseResponseType<E, P>> { if (endpoint.includes('://')) throw new Error('invalid endpoint'); pendingApiRequestsCount.value++; @@ -51,51 +56,11 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin return promise; } -export function apiExternal<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(hostUrl: string, endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal): Promise<Misskey.Endpoints[E]['res']> { - if (!/^https?:\/\//.test(hostUrl)) throw new Error('invalid host name'); - if (endpoint.includes('://')) throw new Error('invalid endpoint'); - pendingApiRequestsCount.value++; - - const onFinally = () => { - pendingApiRequestsCount.value--; - }; - - const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => { - // Append a credential - (data as any).i = token; - - const fullUrl = (hostUrl.slice(-1) === '/' ? hostUrl.slice(0, -1) : hostUrl) - + '/api/' + (endpoint.slice(0, 1) === '/' ? endpoint.slice(1) : endpoint); - // Send request - window.fetch(fullUrl, { - method: 'POST', - body: JSON.stringify(data), - credentials: 'omit', - cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - }, - signal, - }).then(async (res) => { - const body = res.status === 204 ? null : await res.json(); - - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(); - } else { - reject(body.error); - } - }).catch(reject); - }); - - promise.then(onFinally, onFinally); - - return promise; -} - // Implements Misskey.api.ApiClient.request -export function apiGet <E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(endpoint: E, data: P = {} as any): Promise<Misskey.Endpoints[E]['res']> { +export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>( + endpoint: E, + data: P = {} as any, +): Promise<Misskey.api.SwitchCaseResponseType<E, P>> { pendingApiRequestsCount.value++; const onFinally = () => { diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts index 0d6756d498..6f3d3ba8e1 100644 --- a/packages/frontend/src/scripts/autocomplete.ts +++ b/packages/frontend/src/scripts/autocomplete.ts @@ -8,6 +8,8 @@ import getCaretCoordinates from 'textarea-caret'; import { toASCII } from 'punycode/'; import { popup } from '@/os.js'; +export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag'; + export class Autocomplete { private suggestion: { x: Ref<number>; @@ -19,6 +21,7 @@ export class Autocomplete { private currentType: string; private textRef: Ref<string>; private opening: boolean; + private onlyType: SuggestionType[]; private get text(): string { // Use raw .value to get the latest value @@ -35,7 +38,7 @@ export class Autocomplete { /** * 対象のテキストエリアを与えてインスタンスを初期化します。 */ - constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) { + constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, onlyType?: SuggestionType[]) { //#region BIND this.onInput = this.onInput.bind(this); this.complete = this.complete.bind(this); @@ -46,6 +49,7 @@ export class Autocomplete { this.textarea = textarea; this.textRef = textRef; this.opening = false; + this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag']; this.attach(); } @@ -95,7 +99,7 @@ export class Autocomplete { let opened = false; - if (isMention) { + if (isMention && this.onlyType.includes('user')) { const username = text.substring(mentionIndex + 1); if (username !== '' && username.match(/^[a-zA-Z0-9_.]+$/)) { this.open('user', username); @@ -106,7 +110,7 @@ export class Autocomplete { } } - if (isHashtag && !opened) { + if (isHashtag && !opened && this.onlyType.includes('hashtag')) { const hashtag = text.substring(hashtagIndex + 1); if (!hashtag.includes(' ')) { this.open('hashtag', hashtag); @@ -114,7 +118,7 @@ export class Autocomplete { } } - if (isEmoji && !opened) { + if (isEmoji && !opened && this.onlyType.includes('emoji')) { const emoji = text.substring(emojiIndex + 1); if (!emoji.includes(' ')) { this.open('emoji', emoji); @@ -122,7 +126,7 @@ export class Autocomplete { } } - if (isMfmTag && !opened) { + if (isMfmTag && !opened && this.onlyType.includes('mfmTag')) { const mfmTag = text.substring(mfmTagIndex + 1); if (!mfmTag.includes(' ')) { this.open('mfmTag', mfmTag.replace('[', '')); diff --git a/packages/frontend/src/scripts/clear-cache.ts b/packages/frontend/src/scripts/clear-cache.ts new file mode 100644 index 0000000000..f2db87c4fb --- /dev/null +++ b/packages/frontend/src/scripts/clear-cache.ts @@ -0,0 +1,15 @@ +import { unisonReload } from '@/scripts/unison-reload.js'; +import * as os from '@/os.js'; +import { miLocalStorage } from '@/local-storage.js'; +import { fetchCustomEmojis } from '@/custom-emojis.js'; + +export async function clearCache() { + os.waiting(); + miLocalStorage.removeItem('locale'); + miLocalStorage.removeItem('localeVersion'); + miLocalStorage.removeItem('theme'); + miLocalStorage.removeItem('emojis'); + miLocalStorage.removeItem('lastEmojisFetchedAt'); + await fetchCustomEmojis(true); + unisonReload(); +} diff --git a/packages/frontend/src/scripts/emoji-picker.ts b/packages/frontend/src/scripts/emoji-picker.ts new file mode 100644 index 0000000000..f87c3f6fb2 --- /dev/null +++ b/packages/frontend/src/scripts/emoji-picker.ts @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { defineAsyncComponent, Ref, ref } from 'vue'; +import { popup } from '@/os.js'; +import { defaultStore } from '@/store.js'; + +/** + * 絵文字ピッカーを表示する。 + * 類似の機能として{@link ReactionPicker}が存在しているが、この機能とは動きが異なる。 + * 投稿フォームなどで絵文字を選択する時など、絵文字ピックアップ後でもダイアログが消えずに残り、 + * 一度表示したダイアログを連続で使用できることが望ましいシーンでの利用が想定される。 + */ +class EmojiPicker { + private src: Ref<HTMLElement | null> = ref(null); + private manualShowing = ref(false); + private onChosen?: (emoji: string) => void; + private onClosed?: () => void; + + constructor() { + // nop + } + + public async init() { + const emojisRef = defaultStore.reactiveState.pinnedEmojis; + await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { + src: this.src, + pinnedEmojis: emojisRef, + asReactionPicker: false, + manualShowing: this.manualShowing, + choseAndClose: false, + }, { + done: emoji => { + if (this.onChosen) this.onChosen(emoji); + }, + close: () => { + this.manualShowing.value = false; + }, + closed: () => { + this.src.value = null; + if (this.onClosed) this.onClosed(); + }, + }); + } + + public show( + src: HTMLElement, + onChosen?: EmojiPicker['onChosen'], + onClosed?: EmojiPicker['onClosed'], + ) { + this.src.value = src; + this.manualShowing.value = true; + this.onChosen = onChosen; + this.onClosed = onClosed; + } +} + +export const emojiPicker = new EmojiPicker(); diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts index 4159da84c8..8885bf4b7f 100644 --- a/packages/frontend/src/scripts/emojilist.ts +++ b/packages/frontend/src/scripts/emojilist.ts @@ -43,3 +43,9 @@ export function getEmojiName(char: string): string | null { return emojilist[idx].name; } } + +export interface CustomEmojiFolderTree { + value: string; + category: string; + children: CustomEmojiFolderTree[]; +} diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index 87f3886847..d6a5b00c0b 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -82,7 +82,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss to: `/my/drive/file/${file.id}`, text: i18n.ts._fileViewer.title, icon: 'ph-file-text ph-bold ph-lg', - }, null, { + }, { type: 'divider' }, { text: i18n.ts.rename, icon: 'ph-textbox ph-bold ph-lg', action: () => rename(file), @@ -101,7 +101,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss aspectRatio: NaN, uploadFolder: folder ? folder.id : folder, }), - }] : [], null, { + }] : [], { type: 'divider' }, { text: i18n.ts.createNoteFromTheFile, icon: 'ph-pencil ph-bold ph-lg', action: () => os.post({ @@ -118,7 +118,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss text: i18n.ts.download, icon: 'ph-download ph-bold ph-lg', download: file.name, - }, null, { + }, { type: 'divider' }, { text: i18n.ts.delete, icon: 'ph-trash ph-bold ph-lg', danger: true, @@ -126,7 +126,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss }]; if (defaultStore.state.devMode) { - menu = menu.concat([null, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ph-identification-card ph-bold ph-lg', text: i18n.ts.copyFileId, action: () => { diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index e64c08c0ab..e23986ea4a 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -18,6 +18,7 @@ import { getUserMenu } from '@/scripts/get-user-menu.js'; import { clipsCache } from '@/cache.js'; import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; +import { isSupportShare } from '@/scripts/navigator.js'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -60,7 +61,7 @@ export async function getNoteClipMenu(props: { }, ); }, - })), null, { + })), { type: 'divider' }, { icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.createNew, action: async () => { @@ -93,7 +94,7 @@ export async function getNoteClipMenu(props: { }]; } -export function getAbuseNoteMenu(note: misskey.entities.Note, text: string): MenuItem { +export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem { return { icon: 'ph-warning-circle ph-bold ph-lg', text, @@ -107,7 +108,7 @@ export function getAbuseNoteMenu(note: misskey.entities.Note, text: string): Men }; } -export function getCopyNoteLinkMenu(note: misskey.entities.Note, text: string): MenuItem { +export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): MenuItem { return { icon: 'ph-link ph-bold ph-lg', text, @@ -285,7 +286,7 @@ export function getNoteMenu(props: { text: i18n.ts.unclip, danger: true, action: unclip, - }, null] : [] + }, { type: 'divider' }] : [] ), { icon: 'ph-info ph-bold ph-lg', text: i18n.ts.details, @@ -302,20 +303,20 @@ export function getNoteMenu(props: { icon: 'ph-arrow-square-out ph-bold ph-lg', text: i18n.ts.showOnRemote, action: () => { - window.open(appearNote.url ?? appearNote.uri, '_blank'); + window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, } : undefined, - { + ...(isSupportShare() ? [{ icon: 'ph-share-network ph-bold ph-lg', text: i18n.ts.share, action: share, - }, + }] : []), $i && $i.policies.canUseTranslator && instance.translatorAvailable ? { icon: 'ph-translate ph-bold ph-lg', text: i18n.ts.translate, action: translate, } : undefined, - null, + { type: 'divider' }, statePromise.then(state => state.isFavorited ? { icon: 'ph-star-half ph-bold ph-lg', text: i18n.ts.unfavorite, @@ -362,7 +363,7 @@ export function getNoteMenu(props: { }, /* ...($i.isModerator || $i.isAdmin ? [ - null, + { type: 'divider' }, { icon: 'ph-megaphone ph-bold ph-lg', text: i18n.ts.promote, @@ -371,13 +372,13 @@ export function getNoteMenu(props: { : [] ),*/ ...(appearNote.userId !== $i.id ? [ - null, + { type: 'divider' }, appearNote.userId !== $i.id ? getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse) : undefined, ] : [] ), ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ - null, + { type: 'divider' }, appearNote.userId === $i.id ? { icon: 'ph-pencil ph-bold ph-lg', text: i18n.ts.edit, @@ -415,14 +416,14 @@ export function getNoteMenu(props: { icon: 'ph-arrow-square-out ph-bold ph-lg', text: i18n.ts.showOnRemote, action: () => { - window.open(appearNote.url ?? appearNote.uri, '_blank'); + window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, } : undefined] .filter(x => x !== undefined); } if (noteActions.length > 0) { - menu = menu.concat([null, ...noteActions.map(action => ({ + menu = menu.concat([{ type: "divider" }, ...noteActions.map(action => ({ icon: 'ph-plug ph-bold ph-lg', text: action.title, action: () => { @@ -432,7 +433,7 @@ export function getNoteMenu(props: { } if (defaultStore.state.devMode) { - menu = menu.concat([null, { + menu = menu.concat([{ type: "divider" }, { icon: 'ph-identification-card ph-bold ph-lg', text: i18n.ts.copyNoteId, action: () => { @@ -518,7 +519,7 @@ export function getRenoteMenu(props: { }]); } - if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) { + if (!appearNote.channel || appearNote.channel.allowRenoteToExternal) { normalRenoteItems.push(...[{ text: i18n.ts.renote, icon: 'ti ti-repeat', @@ -561,10 +562,9 @@ export function getRenoteMenu(props: { }]); } - // nullを挟むことで区切り線を出せる const renoteItems = [ ...normalRenoteItems, - ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [null] : [], + ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] : [], ...channelRenoteItems, ]; diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 41d0df1b72..67bc781aef 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -119,7 +119,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router userId: user.id, }); } - + async function invalidateFollow() { if (!await getConfirmed(i18n.ts.breakFollowConfirm)) return; @@ -189,7 +189,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; os.post({ specified: user, initialText: `${canonical} ` }); }, - }, null, { + }, { type: 'divider' }, { icon: 'ph-pencil ph-bold ph-lg', text: i18n.ts.editMemo, action: () => { @@ -313,7 +313,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router }]); //} - menu = menu.concat([null, { + menu = menu.concat([{ type: 'divider' }, { icon: user.isMuted ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg', text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, action: toggleMute, @@ -335,7 +335,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router }]); } - menu = menu.concat([null, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ph-warning-circle ph-bold ph-lg', text: i18n.ts.reportAbuse, action: reportAbuse, @@ -343,15 +343,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router } if (user.host !== null) { - menu = menu.concat([null, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ph-arrows-counter-clockwise ph-bold ph-lg', text: i18n.ts.updateRemoteUser, action: userInfoUpdate, }]); } - + if (defaultStore.state.devMode) { - menu = menu.concat([null, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ph-identification-card ph-bold ph-lg', text: i18n.ts.copyUserId, action: () => { @@ -361,7 +361,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router } if ($i && meId === user.id) { - menu = menu.concat([null, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ph-pencil ph-bold ph-lg', text: i18n.ts.editProfile, action: () => { @@ -371,7 +371,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router } if (userActions.length > 0) { - menu = menu.concat([null, ...userActions.map(action => ({ + menu = menu.concat([{ type: 'divider' }, ...userActions.map(action => ({ icon: 'ph-plug ph-bold ph-lg', text: action.title, action: () => { diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts index 0567f3b34a..dc0e90d20a 100644 --- a/packages/frontend/src/scripts/isFfVisibleForMe.ts +++ b/packages/frontend/src/scripts/isFfVisibleForMe.ts @@ -6,11 +6,19 @@ import * as Misskey from 'misskey-js'; import { $i } from '@/account.js'; -export function isFfVisibleForMe(user: Misskey.entities.UserDetailed): boolean { +export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean { if ($i && $i.id === user.id) return true; - if (user.ffVisibility === 'private') return false; - if (user.ffVisibility === 'followers' && !user.isFollowing) return false; + if (user.followingVisibility === 'private') return false; + if (user.followingVisibility === 'followers' && !user.isFollowing) return false; + + return true; +} +export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean { + if ($i && $i.id === user.id) return true; + + if (user.followersVisibility === 'private') return false; + if (user.followersVisibility === 'followers' && !user.isFollowing) return false; return true; } diff --git a/packages/frontend/src/scripts/media-has-audio.ts b/packages/frontend/src/scripts/media-has-audio.ts new file mode 100644 index 0000000000..3421a38a76 --- /dev/null +++ b/packages/frontend/src/scripts/media-has-audio.ts @@ -0,0 +1,9 @@ +export default async function hasAudio(media: HTMLMediaElement) { + const cloned = media.cloneNode() as HTMLMediaElement; + cloned.muted = (cloned as typeof cloned & Partial<HTMLVideoElement>).playsInline = true; + cloned.play(); + await new Promise((resolve) => cloned.addEventListener('playing', resolve)); + const result = !!(cloned as any).audioTracks?.length || (cloned as any).mozHasAudio || !!(cloned as any).webkitAudioDecodedByteCount; + cloned.remove(); + return result; +} diff --git a/packages/frontend/src/scripts/navigator.ts b/packages/frontend/src/scripts/navigator.ts new file mode 100644 index 0000000000..b13186a10e --- /dev/null +++ b/packages/frontend/src/scripts/navigator.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function isSupportShare(): boolean { + return 'share' in navigator; +} diff --git a/packages/frontend/src/scripts/page-metadata.ts b/packages/frontend/src/scripts/page-metadata.ts index 330ba8da83..369e46aae1 100644 --- a/packages/frontend/src/scripts/page-metadata.ts +++ b/packages/frontend/src/scripts/page-metadata.ts @@ -15,6 +15,7 @@ export type PageMetadata = { icon?: string | null; avatar?: Misskey.entities.User | null; userName?: Misskey.entities.User | null; + needWideArea?: boolean; }; export function definePageMetadata(metadata: PageMetadata | null | Ref<PageMetadata | null> | ComputedRef<PageMetadata | null>): void { diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts new file mode 100644 index 0000000000..80441caf15 --- /dev/null +++ b/packages/frontend/src/scripts/post-message.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const postMessageEventTypes = [ + 'misskey:shareForm:shareCompleted', +] as const; + +export type PostMessageEventType = typeof postMessageEventTypes[number]; + +export type MiPostMessageEvent = { + type: PostMessageEventType; + payload?: any; +}; + +/** + * 親フレームにイベントを送信 + */ +export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void { + window.postMessage({ + type, + payload, + }, '*'); +} diff --git a/packages/frontend/src/scripts/reaction-picker.ts b/packages/frontend/src/scripts/reaction-picker.ts index 19e1bfba2c..9b13e794f5 100644 --- a/packages/frontend/src/scripts/reaction-picker.ts +++ b/packages/frontend/src/scripts/reaction-picker.ts @@ -5,6 +5,7 @@ import { defineAsyncComponent, Ref, ref } from 'vue'; import { popup } from '@/os.js'; +import { defaultStore } from '@/store.js'; class ReactionPicker { private src: Ref<HTMLElement | null> = ref(null); @@ -17,25 +18,27 @@ class ReactionPicker { } public async init() { + const reactionsRef = defaultStore.reactiveState.reactions; await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { src: this.src, + pinnedEmojis: reactionsRef, asReactionPicker: true, manualShowing: this.manualShowing, }, { done: reaction => { - this.onChosen!(reaction); + if (this.onChosen) this.onChosen(reaction); }, close: () => { this.manualShowing.value = false; }, closed: () => { this.src.value = null; - this.onClosed!(); + if (this.onClosed) this.onClosed(); }, }); } - public show(src: HTMLElement, onChosen: ReactionPicker['onChosen'], onClosed: ReactionPicker['onClosed']) { + public show(src: HTMLElement, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) { this.src.value = src; this.manualShowing.value = true; this.onChosen = onChosen; diff --git a/packages/frontend/src/scripts/snowfall-effect.ts b/packages/frontend/src/scripts/snowfall-effect.ts new file mode 100644 index 0000000000..a09f02cec0 --- /dev/null +++ b/packages/frontend/src/scripts/snowfall-effect.ts @@ -0,0 +1,476 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SnowfallEffect { + private VERTEX_SOURCE = `#version 300 es + in vec4 a_position; + in vec4 a_color; + in vec3 a_rotation; + in vec3 a_speed; + in float a_size; + out vec4 v_color; + out float v_rotation; + uniform float u_time; + uniform mat4 u_projection; + uniform vec3 u_worldSize; + uniform float u_gravity; + uniform float u_wind; + + void main() { + v_color = a_color; + v_rotation = a_rotation.x + u_time * a_rotation.y; + + vec3 pos = a_position.xyz; + + float turbulence = 1.0; + + pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x; + pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y; + + pos.x += sin(u_time * a_speed.z * turbulence) * a_rotation.z; + pos.z += cos(u_time * a_speed.z * turbulence) * a_rotation.z; + + gl_Position = u_projection * vec4(pos.xyz, a_position.w); + gl_PointSize = (a_size / gl_Position.w) * 100.0; + } + `; + + private FRAGMENT_SOURCE = `#version 300 es + precision highp float; + + in vec4 v_color; + in float v_rotation; + uniform sampler2D u_texture; + out vec4 out_color; + + void main() { + vec2 rotated = vec2( + cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5, + cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5 + ); + + vec4 snowflake = texture(u_texture, rotated); + + out_color = vec4(snowflake.rgb * v_color.xyz, snowflake.a * v_color.a); + } + `; + + private gl: WebGLRenderingContext; + private program: WebGLProgram; + private canvas: HTMLCanvasElement; + private buffers: Record<string, { + size: number; + value: number[] | Float32Array; + location: number; + ref: WebGLBuffer; + }>; + private uniforms: Record<string, { + type: string; + value: number[] | Float32Array; + location: WebGLUniformLocation; + }>; + private texture: WebGLTexture; + private camera: { + fov: number; + near: number; + far: number; + aspect: number; + z: number; + }; + private wind: { + current: number; + force: number; + target: number; + min: number; + max: number; + easing: number; + }; + private time: { + start: number; + previous: number; + } = { + start: 0, + previous: 0, + }; + private raf = 0; + + private density: number = 1 / 90; + private depth = 100; + private count = 1000; + private gravity = 100; + private speed: number = 1 / 10000; + private color: number[] = [1, 1, 1]; + private opacity = 1; + private size = 4; + private snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAErRJREFUeAHdmgnYlmPax5MShaxRKRElPmXJXpaSsRxDU0bTZ+kt65RloiRDltEMQsxYKmS+zzYjxCCamCzV2LchResMIxFRQ1G93+93Pdf5dL9v7zuf4/hm0fc/jt9znddy3/e1nNd53c/7vHXq/AtVWVnZA/bzkaQjoWG298DeMdvrmP6/EIOqC4fBsbAx7Arz4TaYBPXgWVDnO2jSBrB2T0IMIA9mCmmoE8aonPkR6WPZHlp9xSlfeyeBzq9bHBD5feEdUGfDXBgBqnde+a2wvw/dYdNctvZNAp1PnTaFttA6JgP7eVgBM0CNzgO9HNvy0AcYDda6SaDTdXOnz8X+IkZDugAGQmOYA+ob6Ah/MIOMDRPhJjgJ6uV7pXtWt81/50SnY/Wvwn4ZDHAvwJ9ATYcxyaqsnEnqZCyCPaE80BgYZXG/5A3VyyP/b08LHa11z9KmFUwA5eqruRBHYX1s8WSI1Xcbme8Mt8PWUCU+kF8XbFN+dtH+p06OD4IU8EjD/VOZ5bnezq0XHcHuC2oV7BDlkVIWq56uIX8UjAO31GRIMYW0Vo/xXtSXJyTuXVO6xk1qalRTmQ9AfqzEvog2XYpllnsd6Qr4unCPT7NtByu0uU7vuAaOoy1JuvfXpJdTvSX0gI1gCXwGZdFmEFxoQb7Wid8s7lNu+I8wuHGsTqz2zpQ9DAa5R6HC55A2gvCMXthvwi25bjx26H0M9/9f4Rnok9s0zulFlC2HzzP9cnld8nH/p7DVrbmuIfYs6JLz9U3/z+KGadDeCDsmwre7GyEifn/su8HVSsL2HeBn8CK8AW+B7u9R5yrPgyOjvSn5DWAaXAG2UU7CE9Ayt4k4sR1lX4LaLdd9gn2ftsL+Vtuh1Dp/elH1C8lvCdUj8kDK3gbP8XdhCnSC86rcsNSR9pQvhc/gVlB9bUfqoFNAy/mLrUROrpMwCtpBxBbTtLqkF4K6IF9rf57I9pnYekx5AS0P1VhopXso9pR5buC7+kewU86nFcB+BT4EXdIvNO73sRBubGTXLZtTtgp+DEb++bACdqBuJOlAaMMzLVM3whegNznQDtCb+pW5b8YY76euB5+7pxm0IbzCfS8m3Zf2q4T8/+4JNArXGoptpxz8LqDmQJq0Qnostt/sfIn5GygD4/Zeq7B7wljQO2yjB/QGj0Pjxz4wGdqXrkjXtCT/ISyDa6EPpHrSraFjvnecFpMoMx40Br3xSlD262rYObevddHTs2kYwWUG9uP5It/f1eU5Xw9btwoXPALbwYXcg+unG/KB3Rq8n9ddAOpn4Kr8BAaBcltcDo9D7Ouavig1o34x7F94xqPk74eLQH0MH8HvwS3SLPe9iheEG6f70KiuLpZv6sxG/Va5bFJOabaO7ucAvGEbeAH+AN1hV7iDOidQFz4A2oJb6D1YDhXZHkTqpL8EbqHDYRtwW20AsdIb8syl5N2e6dTAPB2mWYa+hE4Qk7I59iMwFZ70GlJlfyuTVfygs7Hyw7HbwI0w3Tak14BqEtdg7wVdIx8pZbtBUbrjZeA3vUPBANkU+sEehev8O4Db6QpwYm+D8II0KPKHwUFeQ3oLDIMN4WgID1yOPQ+MAXMhNAtju3ztmtuAypiAw7EXwo/Am+0NfUG5mknYc6GfGVIjsoFNuyuoh8COuDcd2LmwA9jWE8bB3Q7N4XrwWAz5XOXR+Tx4n6FgdHeB6sF/w2QwhlSXdXvl/jixx4NH8GW5LDzb7GrR4ES4F5QddB99CieAwStOAPegdUZ2B71F3AXbQSn3vJ1bYaYWrayh3NUPTcbYFExVW3CfXwlvgfoavMbnDAY9dxGo6dCt0LeaB54H4UydDEPA2R4PDlrFLB9XuNmTlO+Xr7X9ZNBr9J4+EN8AMcv6ButpMND9FM6EnTOHkLrSnvtzwbbq3vwMB2ow/qWFSC8ZC++ZQaldbquH2afQWbl8TdcvVtC6LtipifAuOKt6gA9Tzqgzb5R2gP1hX3DVtZVHVvdklY5DA5beIkVPuZn8LOgAnWEfeAaUkxCan/voBNkfF+U5cFu5z5XlxZU20OmZtgm1K45VO4naNCukrcBZVk/CD+E/YBjoYjXJY8Zg9DxsDrbbBHTRotxOrug4eBs+hHgWZtKzfHrdXHBi9gDvqzxFHNA5KVfyBCf0ExgB7nkXStLLEKkniNf0AzUs5+ublkVFKiC9FBZAvGxshT0NnN3zoSUYSJQPcjAvm0HmjcIPemNS96F6E36drFLwugx7EEzNZV/l9IjoEPkW4B7eFtYH9QKcBcfA/aCWgpPQOT+zMbb9fS3nDbYR2MdgV0S5aVlUhLs0w45IHi7sqnnGJ2E7CXqHWgZXgJ1y8KqpDUmfSLmSV5yB/XrpDqVP8ofmehNdOv7I0ShfP4yyJdl2a4SchI1gCXgkHgljYfvc1i3cs/SU1A9jQRpfri/b0Sal1RrtSj4ULyHprY5C6+6E1+EBULq0E+DK7A96iwqX0z4td8B3dCdob5gD3UB3j9fUcNuDKFOvgc+bZAZFf4Zgu/q/AGPMgfm+5ShPWay+k6I31BwAvVDRYL2cuqfUVTkfnTqvVFx5ai7/MXn3tp1UrtRkDWRsaAMjzaD08uJ1irz7+8ps/6ZYj90V3FKrQBkvmubULbN7vs7tZRyJV9w0ePLbQ4PcJspqXnkbhbgoGk/AVptZRxpB0hU7Mpc1x34cdgKPm1dzeTts9XPwlFAO5Au4BDbO7ZycO7J9A/Zh2b4A2+ucALefWpTrflDKVq4kHQBOoi9PO1qvsDeGd6AxXAJbQ5VxlFrW8EnDcJlTsOPcjElxL7WNy7AduC4f2+A/rSN/Hyg7YMBTxgqPUT3F2HAqtIb58GvQW86GqyG+ff4UWz0FBuH4UhaTal1vmAGfg98dfP4d4HPGwmwYAg+D2/J7uU0ap/YaolHZVbBj5d1DaSK8ADsmqiH2JIhgNRhbPZrbhSdZ5heVJGw7477VfYuaagMK2sM8iMloga1HXAt/AeWELgQnR/0Z7k3W6pe3xTn/JamTFPGnPMZSj6p90rA8YOziwHcnH/EgTovJlJ0LPSHkyrTKmZNJ+8KrYKBsCQeB0pWdBFNleieMgzjL44jejTK1CPSY0CiMdyOT09g6ni5O3Ceg51U4VNLaPSA3SDNEwwiKFdgHgANNrpjb7UVejYTYCuZ92DR42HYh8gfDJfAMqBi4dqxk+RrKGkD0YXNsA6AT5qCUXhBe5CR0gPCC4dhqKFwI1m1qX0hr94CotDE4aAd3PCyBX4Jyn+sNL5tBDsRAp3S7b5KVYwa2A0nHaO5AXBeDtnlMxizsW+HomLh8zX9R5sTeBSEn/cqc2Tvak9eDXCyP2PgbYWzn2gefHxT7+0Qu/h18DO7XmPWYcYqSXuHz2myb6G7RNs7meLgeMxXugbiPA3clQx0xtgNPGN819L7+oCzvm6zSx+EkI+Du3Pe0LbOd/jqc7dhG9Wib+mJ5jaJBuL8e4B5aAMpAomKlb8d+KZWUVnw+dgzKSdDtvKaLDyJ1ReZB7O0J2EV5Xwd8OsTJExNpu7Q1SJ8zgy7K93UCX4P4mr4udoyhPGDKygOP+tomIFarMw2d+cfgF2DnDVAGoBvzw33YTHgPDoXQ7Fx/Wy6YkdMrcrmrehO4Pz3WvP90cIVPgonwITg4973yu0XTZK0+ZQaQd+K816twVAwKO71ZRj9zeg7lcVzXHghpVN4n2G3BAHQ1NILx4MBjoppgLwL3Ww8IHZsf6vGk3O8fwx9heK7rhD0o2zdg75JtT6GzQQ8KzcZwElSr3M5J85ktYCzEG+Gx2NNzm/Cm5pSp+K2gfLrZbg3RcB2IQcZN1qPM3+l06SjbAltX/TiXe1wtg7+AdR+AcgIs7xUPw94XxuTrnOD4E1bEoe9Rptw+DWGOGeQi7JOs1SfKKfk+epcakPNxbI8uFVdem8vT6aJdq7jASYjOFPdQDP4Q6t+Em8HVutmbkbYH9Tv4LcQW+H6ujy9Wrtxc6A7vQnznb5TbHUPZ0mw7CeoaOBAegmfBIKw8WZzs34M/oNiPGPzB2KHdrVMUlD29VFLLpw2jMWmnaIbdDNxXur+dWgVumTMglI4zMgbUEV5LmjqW7XnRkDS9qhbu/xZlZ8LWuc3UfM22Of80aVcYDJ/lstdIWxXu0TGXm/TO19vveHWuOglUxOo6iMfyBe7JOEp01ech9puuuBCMA8pVcUUNUB5lqgMYwJyE1oXOGTh9v1gO6kmogKEwHtREMHYofz5zAl3lJ2AWqJfgfohJiKB8HWWfg54YA9Zr1fn5Xmm80SdvHhNwVmq2umF8vWxA+WRwwE9BPNhOulrq0nxz97j6Go6DF8HYcBfYyer6MwWuoINeDG6roq4iE97QCtsJuxWc2JrkCeKEbgX7waOgnLiavxdQEWfohtgRwCrygIoxoQv1K0FNgR7gAKPTB+dr5lAWMliqmbAb7AzbgCs42vYK21NmOiwHJ9atpdxqDlhdA75QdYJT4XUYDfbBiVRe5ySoZTAbBpeekp6T4lo5uFnBz0fpJ6P8E9SJufEdXHipdRA/mw2hzmvfhrfgfjCKPwJnwn2g3igldb4hNaD5a6/fz7eHVuAb2wPwPs+4DB7E/hTagd64BbgoC6Ab9IAfgn+OX0p/ppAaGxZjnw6+Ep8DK8Cj0IDrmHw3GaeN9EZ/AlxFfk1RuVGUYu8K00D9Fa6EvrAUVKzO29gXg9vC1VW3g540w0xBcU2hKJnz+FxYvTCXWaduK/StuTZlLcD6JjnfEvsb6A56m32z78q4FMGw1gA4lEa60WmwMeiSnsljIBSDmEOBE3RdfvggbMuMIbNhItgJtbyUpE9ddjA0Bid1sderXDaQ1OdPAO9zH6hDcpuG2Ml7SQfArHRx6Xpf3JTluySrsrIP6Seg9/iMqsEvF6YZoXIDeAZCRmpneAHEnnLQnaEuXATX53schR3n/e7YyuvOT1bpnyV107Io3xZ6QWs4EirAyXkEqqvK3xa9CQ0c5C5xQ+zN8kWjcr2xZxTsBHfmsipbP671ZmW3wHYA58DdEPobhtwVF2HfBE9H3pT8xjkdja3iiDK4PQBO8Dx4B9wiH8JKeANcKTUW9IITwKNMeYrcArfDhVDsb1pVyty26le5D97/zWzrzVUGXyVjI0WjHUgq4CjoAuGiRuuJkN7mSJX7cn+uaZNyfBBgDHZqXvqsU2cZ6aPwChgE/ap8M9wLbSH+0DKOaw18z8N12GPAyf4BfADbwBmwCbxAHY9NvxQXx2GgVLZXPvurZDE0rqk5+NmAm8U2aIbdH9yDalgpSS80ltlB29fPqW9c8XLUHnsIuGquqt8gN7edwtazrOsAn4MysLryX8BD4Ap3y+0dZROIwPsl9h/hHjgit4lXdrdvHN8dc91wyk7JdvIS7VpF46Jb2ZGz4WJIRyBpBKQW3oR8lZuSvwQMhKtAfQUpYuf27cgbNx6EEeDAzgMHPwYMYi2gEcSfxC7B9qicDMoo/1vQI8p9IG88WAY/yeVpYrJdHpf5vytu4Ky7X46xIamrvjDb52OrG3K+HrZt4xq9wYEZPGPVfp7bhsdE2os2ylV6J1n5mbYPUX4S7AkGX+OAk2t6mm1Iw3PtQ+O4LuooK26RYvW3s7nBLZDiAGlbUHYiRV/S5AWk28DTEFqB4eo+B+n1M55Ivhu4kspj92uYCm6Px0Gv61lor0fcDQNBrQQnOr71lVeYsm894L/bkBuFe/u93eBngJtJMlwTDIDKyfDt6n3se8Dt8jHoNU0o70waq34obZ8lPx4coG+LbifrP6Pt0aQvwn65LFzcAHY8ZUtgAnwExp2WoMpeQLvaA12p7bf/pLPFmS3a/ajr750cfE43wX4YYmU9wi7IddHBCsrc69vm8uuwQydYVhQVvmsUn7s+ebfD0GhXrI+yf2jqA4oPKdo+iHxMwHbYRmgjta4cUTqCWXkg0UHatIR4SxxWKK9PeXhgKiZfxWOthzXuGff4p6b54bH3Y3W3pNxJcK8ebgdI44iys0G0N/8qKGOAGg9Ni50n3yjy2GkxSKtMRtT/21I7Fg/H9lRIX6qK5YX6zSjvDL4BGiBfBnUNmFdzwfKX4Ct40OtJv1sDj0Hlzrk6xbM3tob7uCf4amyk96VHvQg7gltGzQG9wpcwX6BCesfJ3/kJiMmgs+Gm4errUeZqF+Up4IoOzoWLcmqETyLve/2BsKkFpGUvK7VYCz6j06RbQx+ogHhN3Qdb3QF+a/wVKF94OhSHR77sWcXytcKm82usHGW9QE2B3skq/QB7APaqnJ9NuvaufnF1GIhxYH3LSAeA+hM0hMfgNzATdHvjgDHDv+qkP8gW77XW2gwmYsJe2F3zZDgxI7NteTo+/1WD/B9Au3Zjh2RyrgAAAABJRU5ErkJggg=='; + + private INITIAL_BUFFERS = () => ({ + position: { size: 3, value: [] }, + color: { size: 4, value: [] }, + size: { size: 1, value: [] }, + rotation: { size: 3, value: [] }, + speed: { size: 3, value: [] }, + }); + + private INITIAL_UNIFORMS = () => ({ + time: { type: 'float', value: 0 }, + worldSize: { type: 'vec3', value: [0, 0, 0] }, + gravity: { type: 'float', value: this.gravity }, + wind: { type: 'float', value: 0 }, + projection: { + type: 'mat4', + value: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], + }, + }); + + private UNIFORM_SETTERS = { + int: 'uniform1i', + float: 'uniform1f', + vec2: 'uniform2fv', + vec3: 'uniform3fv', + vec4: 'uniform4fv', + mat2: 'uniformMatrix2fv', + mat3: 'uniformMatrix3fv', + mat4: 'uniformMatrix4fv', + }; + + private CAMERA = { + fov: 60, + near: 5, + far: 10000, + aspect: 1, + z: 100, + }; + + private WIND = { + current: 0, + force: 0.01, + target: 0.01, + min: 0, + max: 0.125, + easing: 0.0005, + }; + + constructor() { + const canvas = this.initCanvas(); + const gl = canvas.getContext('webgl2', { antialias: true }); + if (gl == null) throw new Error('Failed to get WebGL context'); + + document.body.append(canvas); + + this.canvas = canvas; + this.gl = gl; + this.program = this.initProgram(); + this.buffers = this.initBuffers(); + this.uniforms = this.initUniforms(); + this.texture = this.initTexture(); + this.camera = this.initCamera(); + this.wind = this.initWind(); + + this.resize = this.resize.bind(this); + this.update = this.update.bind(this); + + window.addEventListener('resize', () => this.resize()); + } + + private initCanvas(): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + + Object.assign(canvas.style, { + position: 'fixed', + top: 0, + left: 0, + width: '100vw', + height: '100vh', + background: 'transparent', + 'pointer-events': 'none', + 'z-index': 2147483647, + }); + + return canvas; + } + + private initCamera() { + return { ...this.CAMERA }; + } + + private initWind() { + return { ...this.WIND }; + } + + private initShader(type, source): WebGLShader { + const { gl } = this; + const shader = gl.createShader(type); + if (shader == null) throw new Error('Failed to create shader'); + + gl.shaderSource(shader, source); + gl.compileShader(shader); + + return shader; + } + + private initProgram(): WebGLProgram { + const { gl } = this; + const vertex = this.initShader(gl.VERTEX_SHADER, this.VERTEX_SOURCE); + const fragment = this.initShader(gl.FRAGMENT_SHADER, this.FRAGMENT_SOURCE); + const program = gl.createProgram(); + if (program == null) throw new Error('Failed to create program'); + + gl.attachShader(program, vertex); + gl.attachShader(program, fragment); + gl.linkProgram(program); + gl.useProgram(program); + + return program; + } + + private initBuffers(): SnowfallEffect['buffers'] { + const { gl, program } = this; + const buffers = this.INITIAL_BUFFERS() as unknown as SnowfallEffect['buffers']; + + for (const [name, buffer] of Object.entries(buffers)) { + buffer.location = gl.getAttribLocation(program, `a_${name}`); + buffer.ref = gl.createBuffer()!; + + gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref); + gl.enableVertexAttribArray(buffer.location); + gl.vertexAttribPointer( + buffer.location, + buffer.size, + gl.FLOAT, + false, + 0, + 0, + ); + } + + return buffers; + } + + private updateBuffers() { + const { buffers } = this; + + for (const name of Object.keys(buffers)) { + this.setBuffer(name); + } + } + + private setBuffer(name: string, value?) { + const { gl, buffers } = this; + const buffer = buffers[name]; + + buffer.value = new Float32Array(value ?? buffer.value); + + gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref); + gl.bufferData(gl.ARRAY_BUFFER, buffer.value, gl.STATIC_DRAW); + } + + private initUniforms(): SnowfallEffect['uniforms'] { + const { gl, program } = this; + const uniforms = this.INITIAL_UNIFORMS() as unknown as SnowfallEffect['uniforms']; + + for (const [name, uniform] of Object.entries(uniforms)) { + uniform.location = gl.getUniformLocation(program, `u_${name}`)!; + } + + return uniforms; + } + + private updateUniforms() { + const { uniforms } = this; + + for (const name of Object.keys(uniforms)) { + this.setUniform(name); + } + } + + private setUniform(name: string, value?) { + const { gl, uniforms } = this; + const uniform = uniforms[name]; + const setter = this.UNIFORM_SETTERS[uniform.type]; + const isMatrix = /^mat[2-4]$/i.test(uniform.type); + + uniform.value = value ?? uniform.value; + + if (isMatrix) { + gl[setter](uniform.location, false, uniform.value); + } else { + gl[setter](uniform.location, uniform.value); + } + } + + private initTexture() { + const { gl } = this; + const texture = gl.createTexture(); + if (texture == null) throw new Error('Failed to create texture'); + const image = new Image(); + + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + 1, + 1, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + new Uint8Array([0, 0, 0, 0]), + ); + + image.onload = () => { + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + gl.RGBA, + gl.UNSIGNED_BYTE, + image, + ); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + }; + + image.src = this.snowflake; + + return texture; + } + + private initSnowflakes(vw: number, vh: number, dpi: number) { + const position: number[] = []; + const color: number[] = []; + const size: number[] = []; + const rotation: number[] = []; + const speed: number[] = []; + + const height = 1 / this.density; + const width = (vw / vh) * height; + const depth = this.depth; + const count = this.count; + const length = (vw / vh) * count; + + for (let i = 0; i < length; ++i) { + position.push( + -width + Math.random() * width * 2, + -height + Math.random() * height * 2, + Math.random() * depth * 2, + ); + + speed.push(1 + Math.random(), 1 + Math.random(), Math.random() * 10); + + rotation.push( + Math.random() * 2 * Math.PI, + Math.random() * 20, + Math.random() * 10, + ); + + color.push(...this.color, 0.1 + Math.random() * this.opacity); + //size.push((this.size * Math.random() * this.size * vh * dpi) / 1000); + size.push((this.size * vh * dpi) / 1000); + } + + this.setUniform('worldSize', [width, height, depth]); + + this.setBuffer('position', position); + this.setBuffer('color', color); + this.setBuffer('rotation', rotation); + this.setBuffer('size', size); + this.setBuffer('speed', speed); + } + + private setProjection(aspect: number) { + const { camera } = this; + + camera.aspect = aspect; + + const fovRad = (camera.fov * Math.PI) / 180; + const f = Math.tan(Math.PI * 0.5 - 0.5 * fovRad); + const rangeInv = 1.0 / (camera.near - camera.far); + + const m0 = f / camera.aspect; + const m5 = f; + const m10 = (camera.near + camera.far) * rangeInv; + const m11 = -1; + const m14 = camera.near * camera.far * rangeInv * 2 + camera.z; + const m15 = camera.z; + + return [m0, 0, 0, 0, 0, m5, 0, 0, 0, 0, m10, m11, 0, 0, m14, m15]; + } + + public render() { + const { gl } = this; + + gl.enable(gl.BLEND); + gl.enable(gl.CULL_FACE); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE); + gl.disable(gl.DEPTH_TEST); + + this.updateBuffers(); + this.updateUniforms(); + this.resize(true); + + this.time = { + start: window.performance.now(), + previous: window.performance.now(), + }; + + if (this.raf) window.cancelAnimationFrame(this.raf); + this.raf = window.requestAnimationFrame(this.update); + + return this; + } + + private resize(updateSnowflakes = false) { + const { canvas, gl } = this; + const vw = canvas.offsetWidth; + const vh = canvas.offsetHeight; + const aspect = vw / vh; + const dpi = window.devicePixelRatio; + + canvas.width = vw * dpi; + canvas.height = vh * dpi; + + gl.viewport(0, 0, vw * dpi, vh * dpi); + gl.clearColor(0, 0, 0, 0); + + if (updateSnowflakes === true) { + this.initSnowflakes(vw, vh, dpi); + } + + this.setUniform('projection', this.setProjection(aspect)); + } + + private update(timestamp: number) { + const { gl, buffers, wind } = this; + const elapsed = (timestamp - this.time.start) * this.speed; + const delta = timestamp - this.time.previous; + + gl.clear(gl.COLOR_BUFFER_BIT); + gl.drawArrays( + gl.POINTS, + 0, + buffers.position.value.length / buffers.position.size, + ); + + if (Math.random() > 0.995) { + wind.target = + (wind.min + Math.random() * (wind.max - wind.min)) * + (Math.random() > 0.5 ? -1 : 1); + } + + wind.force += (wind.target - wind.force) * wind.easing; + wind.current += wind.force * (delta * 0.2); + + this.setUniform('wind', wind.current); + this.setUniform('time', elapsed); + + this.time.previous = timestamp; + + this.raf = window.requestAnimationFrame(this.update); + } +} diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 4b0cd0bb39..2f7545ef0d 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -3,13 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import type { SoundStore } from '@/store.js'; import { defaultStore } from '@/store.js'; +import * as os from '@/os.js'; -const ctx = new AudioContext(); +let ctx: AudioContext; const cache = new Map<string, AudioBuffer>(); +let canPlay = true; export const soundsTypes = [ + // 音声なし null, + + // ドライブの音声 + '_driveFile_', + + // プリインストール 'syuilo/n-aec', 'syuilo/n-aec-4va', 'syuilo/n-aec-4vb', @@ -38,6 +47,8 @@ export const soundsTypes = [ 'syuilo/waon', 'syuilo/popo', 'syuilo/triple', + 'syuilo/bubble1', + 'syuilo/bubble2', 'syuilo/poi1', 'syuilo/poi2', 'syuilo/pirori', @@ -61,46 +72,161 @@ export const soundsTypes = [ 'noizenecio/kick_gaba7', ] as const; -export async function getAudio(file: string, useCache = true) { - if (useCache && cache.has(file)) { - return cache.get(file)!; +export const operationTypes = [ + 'noteMy', + 'note', + 'antenna', + 'channel', + 'notification', + 'reaction', +] as const; + +/** サウンドの種類 */ +export type SoundType = typeof soundsTypes[number]; + +/** スプライトの種類 */ +export type OperationType = typeof operationTypes[number]; + +/** + * 音声を読み込む + * @param soundStore サウンド設定 + * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする + */ +export async function loadAudio(soundStore: SoundStore, 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 (_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; + } + } + + 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; + } } - const response = await fetch(`/client-assets/sounds/${file}.mp3`); const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await ctx.decodeAudioData(arrayBuffer); - if (useCache) { - cache.set(file, audioBuffer); + if (options?.useCache ?? true) { + if (soundStore.type === '_driveFile_') { + cache.set(soundStore.fileId, audioBuffer); + } else { + cache.set(soundStore.type, audioBuffer); + } } return audioBuffer; } -export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement { - const masterVolume = defaultStore.state.sound_masterVolume; - audio.volume = masterVolume - ((1 - volume) * masterVolume); - return audio; +/** + * 既定のスプライトを再生する + * @param type スプライトの種類を指定 + */ +export function play(operationType: OperationType) { + const sound = defaultStore.state[`sound_${operationType}`]; + if (_DEV_) console.log('play', operationType, sound); + if (sound.type == null || !canPlay) return; + + canPlay = false; + playFile(sound).finally(() => { + // ごく短時間に音が重複しないように + setTimeout(() => { + canPlay = true; + }, 25); + }); } -export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') { - const sound = defaultStore.state[`sound_${type}`]; - if (_DEV_) console.log('play', type, sound); - if (sound.type == null) return; - playFile(sound.type, sound.volume); +/** + * サウンド設定形式で指定された音声を再生する + * @param soundStore サウンド設定 + */ +export async function playFile(soundStore: SoundStore) { + const buffer = await loadAudio(soundStore); + if (!buffer) return; + createSourceNode(buffer, soundStore.volume)?.start(); } -export async function playFile(file: string, volume: number) { +export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null { const masterVolume = defaultStore.state.sound_masterVolume; - if (masterVolume === 0 || volume === 0) { - return; + if (isMute() || masterVolume === 0 || volume === 0) { + return null; } const gainNode = ctx.createGain(); gainNode.gain.value = masterVolume * volume; const soundSource = ctx.createBufferSource(); - soundSource.buffer = await getAudio(file); + soundSource.buffer = buffer; soundSource.connect(gainNode).connect(ctx.destination); - soundSource.start(); + + return soundSource; +} + +/** + * 音声の長さをミリ秒で取得する + * @param file ファイルのURL(ドライブIDではない) + */ +export async function getSoundDuration(file: string): Promise<number> { + const audioEl = document.createElement('audio'); + audioEl.src = file; + return new Promise((resolve) => { + const si = setInterval(() => { + if (audioEl.readyState > 0) { + resolve(audioEl.duration * 1000); + clearInterval(si); + audioEl.remove(); + } + }, 100); + }); +} + +/** + * ミュートすべきかどうかを判断する + */ +export function isMute(): boolean { + if (defaultStore.state.sound_notUseSound) { + // サウンドを出力しない + return true; + } + + // noinspection RedundantIfStatementJS + if (defaultStore.state.sound_useSoundOnlyWhenActive && document.visibilityState === 'hidden') { + // ブラウザがアクティブな時のみサウンドを出力する + return true; + } + + return false; } diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index 22b8a5df37..3bf6d5798c 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -44,7 +44,7 @@ export const getBuiltinThemes = () => Promise.all( 'd-cherry', 'd-ice', 'd-u0', - ].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)), + ].map(name => import(`@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)), ); export const getBuiltinThemesRef = () => { diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 1f0aae3a3a..c86c3d01ad 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -6,6 +6,7 @@ import { markRaw, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { miLocalStorage } from './local-storage.js'; +import type { SoundType } from '@/scripts/sound.js'; import { Storage } from '@/pizzax.js'; interface PostFormAction { @@ -35,6 +36,22 @@ interface PageViewInterruptor { handler: (page: Misskey.entities.Page) => unknown; } +/** サウンド設定 */ +export type SoundStore = { + type: Exclude<SoundType, '_driveFile_'>; + volume: number; +} | { + type: '_driveFile_'; + + /** ドライブのファイルID */ + fileId: string; + + /** ファイルURL(こちらが優先される) */ + fileUrl: string; + + volume: number; +} + export const postFormActions: PostFormAction[] = []; export const userActions: UserAction[] = []; export const noteActions: NoteAction[] = []; @@ -114,6 +131,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'], }, + pinnedEmojis: { + where: 'account', + default: [], + }, reactionAcceptance: { where: 'account', default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null, @@ -181,7 +202,7 @@ export const defaultStore = markRaw(new Storage('base', { tl: { where: 'deviceAccount', default: { - src: 'home' as 'home' | 'local' | 'social' | 'global' | `list:${string}`, + src: 'home' as 'home' | 'local' | 'social' | 'global' | 'bubble' | `list:${string}`, userList: null as Misskey.entities.UserList | null, }, }, @@ -226,10 +247,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, - enableDataSaverMode: { - where: 'device', - default: false, - }, disableShowingAnimatedImages: { where: 'device', default: window.matchMedia('(prefers-reduced-motion)').matches, @@ -286,19 +303,19 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'remote' as 'none' | 'remote' | 'always', }, - reactionPickerSize: { + emojiPickerScale: { where: 'device', default: 1, }, - reactionPickerWidth: { + emojiPickerWidth: { where: 'device', default: 1, }, - reactionPickerHeight: { + emojiPickerHeight: { where: 'device', default: 2, }, - reactionPickerUseDrawerForMobile: { + emojiPickerUseDrawerForMobile: { where: 'device', default: true, }, @@ -362,6 +379,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'medium' as 'small' | 'medium' | 'large', }, + limitWidthOfReaction: { + where: 'device', + default: true, + }, forceShowAds: { where: 'device', default: false, @@ -422,30 +443,55 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: true, }, + dataSaver: { + where: 'device', + default: { + media: false, + avatar: false, + urlPreview: false, + code: false, + } as Record<string, boolean>, + }, + enableSeasonalScreenEffect: { + where: 'device', + default: false, + }, sound_masterVolume: { where: 'device', default: 0.3, }, + sound_notUseSound: { + where: 'device', + default: false, + }, + sound_useSoundOnlyWhenActive: { + where: 'device', + default: false, + }, sound_note: { where: 'device', - default: { type: 'syuilo/n-aec', volume: 0 }, + default: { type: 'syuilo/n-aec', volume: 0 } as SoundStore, }, sound_noteMy: { where: 'device', - default: { type: 'syuilo/n-cea-4va', volume: 1 }, + default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore, }, sound_notification: { where: 'device', - default: { type: 'syuilo/n-ea', volume: 1 }, + default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore, }, sound_antenna: { where: 'device', - default: { type: 'syuilo/triple', volume: 1 }, + default: { type: 'syuilo/triple', volume: 1 } as SoundStore, }, sound_channel: { where: 'device', - default: { type: 'syuilo/square-pico', volume: 1 }, + default: { type: 'syuilo/square-pico', volume: 1 } as SoundStore, + }, + sound_reaction: { + where: 'device', + default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore, }, })); diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index b863f61cb4..fd2716bf9d 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -159,6 +159,10 @@ hr { background: var(--divider); } +rt { + white-space: initial; +} + .ph-bold { width: 1.28em; vertical-align: -12%; diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts index 66061fcd70..f4516bbe5b 100644 --- a/packages/frontend/src/types/menu.ts +++ b/packages/frontend/src/types/menu.ts @@ -8,13 +8,13 @@ import { Ref } from 'vue'; export type MenuAction = (ev: MouseEvent) => void; -export type MenuDivider = null; +export type MenuDivider = { type: 'divider' }; export type MenuNull = undefined; export type MenuLabel = { type: 'label', text: string }; export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean }; export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; -export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean }; +export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean | Ref<boolean> }; export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction }; export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) }; diff --git a/packages/frontend/src/types/page-header.ts b/packages/frontend/src/types/page-header.ts new file mode 100644 index 0000000000..295b97a7fd --- /dev/null +++ b/packages/frontend/src/types/page-header.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export type PageHeaderItem = { + text: string; + icon: string; + highlighted?: boolean; + handler: (ev: MouseEvent) => void; +}; diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index d153f776dd..a3adbfb1b1 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -64,7 +64,7 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.charts, icon: 'ph-chart-line ph-bold ph-lg', to: '/about#charts', - }, null, { + }, { type: 'divider' }, { type: 'link', text: i18n.ts.ads, icon: 'ph-flag ph-bold ph-lg', @@ -79,29 +79,29 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.tools, icon: 'ph-toolbox ph-bold ph-lg', children: toolsMenuItems(), - }, null, (instance.impressumUrl) ? { + }, { type: 'divider' }, (instance.impressumUrl) ? { text: i18n.ts.impressum, icon: 'ph-newspaper-clipping ph-bold ph-lg', action: () => { - window.open(instance.impressumUrl, '_blank'); + window.open(instance.impressumUrl, '_blank', 'noopener'); }, } : undefined, (instance.tosUrl) ? { text: i18n.ts.termsOfService, icon: 'ph-notebook ph-bold ph-lg', action: () => { - window.open(instance.tosUrl, '_blank'); + window.open(instance.tosUrl, '_blank', 'noopener'); }, } : undefined, (instance.privacyPolicyUrl) ? { text: i18n.ts.privacyPolicy, icon: 'ph-shield ph-bold ph-lg', action: () => { - window.open(instance.privacyPolicyUrl, '_blank'); + window.open(instance.privacyPolicyUrl, '_blank', 'noopener'); }, - } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, { + } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, { text: i18n.ts.help, icon: 'ph-question ph-bold ph-lg', action: () => { - window.open('https://misskey-hub.net/help.html', '_blank'); + window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener'); }, }, ($i) ? { text: i18n.ts._initialTutorial.launchTutorial, diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 7f8556d8d2..6b69e1accf 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -63,7 +63,7 @@ const XUpload = defineAsyncComponent(() => import('./upload.vue')); const dev = _DEV_; -let notifications = $ref<Misskey.entities.Notification[]>([]); +const notifications = ref<Misskey.entities.Notification[]>([]); function onNotification(notification: Misskey.entities.Notification, isClient = false) { if (document.visibilityState === 'visible') { @@ -72,13 +72,13 @@ function onNotification(notification: Misskey.entities.Notification, isClient = useStream().send('readNotification'); } - notifications.unshift(notification); + notifications.value.unshift(notification); window.setTimeout(() => { - if (notifications.length > 3) notifications.pop(); + if (notifications.value.length > 3) notifications.value.pop(); }, 500); window.setTimeout(() => { - notifications = notifications.filter(x => x.id !== notification.id); + notifications.value = notifications.value.filter(x => x.id !== notification.id); }, 6000); } diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue index ff980e0d88..c92695afed 100644 --- a/packages/frontend/src/ui/_common_/statusbar-federation.vue +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -47,9 +47,9 @@ const props = defineProps<{ refreshIntervalSec?: number; }>(); -const instances = ref<Misskey.entities.Instance[]>([]); +const instances = ref<Misskey.entities.FederationInstance[]>([]); const fetching = ref(true); -let key = $ref(0); +const key = ref(0); const tick = () => { os.api('federation/instances', { @@ -58,7 +58,7 @@ const tick = () => { }).then(res => { instances.value = res; fetching.value = false; - key++; + key.value++; }); }; diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue index c8e5b3a8af..58e109ad7f 100644 --- a/packages/frontend/src/ui/_common_/statusbar-rss.vue +++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue @@ -44,7 +44,7 @@ const props = defineProps<{ const items = ref([]); const fetching = ref(true); -let key = $ref(0); +const key = ref(0); const tick = () => { window.fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => { @@ -54,7 +54,7 @@ const tick = () => { } items.value = feed.items; fetching.value = false; - key++; + key.value++; }); }); }; diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue index f1fcd315d0..6057174ba8 100644 --- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue +++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue @@ -50,7 +50,7 @@ const props = defineProps<{ const notes = ref<Misskey.entities.Note[]>([]); const fetching = ref(true); -let key = $ref(0); +const key = ref(0); const tick = () => { if (props.userListId == null) return; @@ -59,7 +59,7 @@ const tick = () => { }).then(res => { notes.value = res; fetching.value = false; - key++; + key.value++; }); }; diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue index 0955a71718..4cb773d28a 100644 --- a/packages/frontend/src/ui/_common_/stream-indicator.vue +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onUnmounted } from 'vue'; +import { onUnmounted, ref } from 'vue'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; @@ -23,14 +23,14 @@ import { defaultStore } from '@/store.js'; const zIndex = os.claimZIndex('high'); -let hasDisconnected = $ref(false); +const hasDisconnected = ref(false); function onDisconnected() { - hasDisconnected = true; + hasDisconnected.value = true; } function resetDisconnected() { - hasDisconnected = false; + hasDisconnected.value = false; } function reload() { diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue index bed1b6728f..62531a4f4d 100644 --- a/packages/frontend/src/ui/classic.header.vue +++ b/packages/frontend/src/ui/classic.header.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, onMounted } from 'vue'; +import { computed, defineAsyncComponent, onMounted, ref } from 'vue'; import { openInstanceMenu } from './_common_/common'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar'; @@ -59,12 +59,12 @@ import { i18n } from '@/i18n.js'; const WINDOW_THRESHOLD = 1400; -let settingsWindowed = $ref(window.innerWidth > WINDOW_THRESHOLD); -let menu = $ref(defaultStore.state.menu); +const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD); +const menu = ref(defaultStore.state.menu); // const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); -let otherNavItemIndicated = computed<boolean>(() => { +const otherNavItemIndicated = computed<boolean>(() => { for (const def in navbarItemDef) { - if (menu.includes(def)) continue; + if (menu.value.includes(def)) continue; if (navbarItemDef[def].indicated) return true; } return false; @@ -86,7 +86,7 @@ function openAccountMenu(ev: MouseEvent) { onMounted(() => { window.addEventListener('resize', () => { - settingsWindowed = (window.innerWidth >= WINDOW_THRESHOLD); + settingsWindowed.value = (window.innerWidth >= WINDOW_THRESHOLD); }, { passive: true }); }); diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue index 6e275c5349..4fa8f1b434 100644 --- a/packages/frontend/src/ui/classic.sidebar.vue +++ b/packages/frontend/src/ui/classic.sidebar.vue @@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, onMounted, computed, watch, nextTick } from 'vue'; +import { defineAsyncComponent, computed, watch, ref, shallowRef } from 'vue'; import { openInstanceMenu } from './_common_/common.js'; // import { host } from '@/config.js'; import * as os from '@/os.js'; @@ -65,24 +65,24 @@ import { i18n } from '@/i18n.js'; const WINDOW_THRESHOLD = 1400; -const menu = $ref(defaultStore.state.menu); +const menu = ref(defaultStore.state.menu); const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); const otherNavItemIndicated = computed<boolean>(() => { for (const def in navbarItemDef) { - if (menu.includes(def)) continue; + if (menu.value.includes(def)) continue; if (navbarItemDef[def].indicated) return true; } return false; }); -let el = $shallowRef<HTMLElement>(); +const el = shallowRef<HTMLElement>(); // let accounts = $ref([]); // let connection = $ref(null); -let iconOnly = $ref(false); -let settingsWindowed = $ref(false); +const iconOnly = ref(false); +const settingsWindowed = ref(false); function calcViewState() { - iconOnly = (window.innerWidth <= WINDOW_THRESHOLD) || (menuDisplay.value === 'sideIcon'); - settingsWindowed = (window.innerWidth > WINDOW_THRESHOLD); + iconOnly.value = (window.innerWidth <= WINDOW_THRESHOLD) || (menuDisplay.value === 'sideIcon'); + settingsWindowed.value = (window.innerWidth > WINDOW_THRESHOLD); } function more(ev: MouseEvent) { diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index 8f06f66012..aadeaea46f 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="!showMenuOnTop" class="sidebar"> <XSidebar/> </div> - <div v-else ref="widgetsLeft" class="widgets left"> + <div v-else-if="!pageMetadata?.needWideArea" ref="widgetsLeft" class="widgets left"> <XWidgets place="left" :marginTop="'var(--margin)'" @mounted="attachSticky(widgetsLeft)"/> </div> @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </main> - <div v-if="isDesktop" ref="widgetsRight" class="widgets right"> + <div v-if="isDesktop && !pageMetadata?.needWideArea" ref="widgetsRight" class="widgets right"> <XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--margin)'" @mounted="attachSticky(widgetsRight)"/> </div> </div> @@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, ComputedRef, onMounted, provide } from 'vue'; +import { defineAsyncComponent, onMounted, provide, ref, computed, shallowRef } from 'vue'; import XSidebar from './classic.sidebar.vue'; import XCommon from './_common_/common.vue'; import { instanceName } from '@/config.js'; @@ -62,26 +62,26 @@ const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); const DESKTOP_THRESHOLD = 1100; -let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD); +const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); -let widgetsShowing = $ref(false); -let fullView = $ref(false); -let globalHeaderHeight = $ref(0); +const pageMetadata = ref<null | PageMetadata>(); +const widgetsShowing = ref(false); +const fullView = ref(false); +const globalHeaderHeight = ref(0); const wallpaper = miLocalStorage.getItem('wallpaper') != null; -const showMenuOnTop = $computed(() => defaultStore.state.menuDisplay === 'top'); -let live2d = $shallowRef<HTMLIFrameElement>(); -let widgetsLeft = $ref(); -let widgetsRight = $ref(); +const showMenuOnTop = computed(() => defaultStore.state.menuDisplay === 'top'); +const live2d = shallowRef<HTMLIFrameElement>(); +const widgetsLeft = ref(); +const widgetsRight = ref(); provide('router', mainRouter); provideMetadataReceiver((info) => { - pageMetadata = info; + pageMetadata.value = info.value; if (pageMetadata.value) { document.title = `${pageMetadata.value.title} | ${instanceName}`; } }); -provide('shouldHeaderThin', showMenuOnTop); +provide('shouldHeaderThin', showMenuOnTop.value); provide('forceSpacerMin', true); function attachSticky(el) { @@ -110,10 +110,10 @@ function onContextmenu(ev: MouseEvent) { type: 'label', text: path, }, { - icon: fullView ? 'ph-arrows-in-simple ph-bold ph-lg' : 'ph-frame-corners ph-bold ph-lg', - text: fullView ? i18n.ts.quitFullView : i18n.ts.fullView, + icon: fullView.value ? 'ph-arrows-in-simple ph-bold ph-lg' : 'ph-frame-corners ph-bold ph-lg', + text: fullView.value ? i18n.ts.quitFullView : i18n.ts.fullView, action: () => { - fullView = !fullView; + fullView.value = !fullView.value; }, }, { icon: 'ph-frame-corners ph-bold ph-lg', @@ -154,13 +154,13 @@ defaultStore.loaded.then(() => { onMounted(() => { window.addEventListener('resize', () => { - isDesktop = (window.innerWidth >= DESKTOP_THRESHOLD); + isDesktop.value = (window.innerWidth >= DESKTOP_THRESHOLD); }, { passive: true }); if (defaultStore.state.aiChanMode) { - const iframeRect = live2d.getBoundingClientRect(); + const iframeRect = live2d.value.getBoundingClientRect(); window.addEventListener('mousemove', ev => { - live2d.contentWindow.postMessage({ + live2d.value.contentWindow.postMessage({ type: 'moveCursor', body: { x: ev.clientX - iframeRect.left, @@ -169,7 +169,7 @@ onMounted(() => { }, '*'); }, { passive: true }); window.addEventListener('touchmove', ev => { - live2d.contentWindow.postMessage({ + live2d.value.contentWindow.postMessage({ type: 'moveCursor', body: { x: ev.touches[0].clientX - iframeRect.left, diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 3224944cd6..0df814fc88 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XSidebar v-if="!isMobile"/> <div :class="$style.main"> - <XAnnouncements v-if="$i" :class="$style.announcements"/> + <XAnnouncements v-if="$i"/> <XStatusBars/> <div ref="columnsEl" :class="[$style.sections, { [$style.center]: deckStore.reactiveState.columnAlign.value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel"> <!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> @@ -92,14 +92,13 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, ref, watch } from 'vue'; +import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue'; import { v4 as uuid } from 'uuid'; import XCommon from './_common_/common.vue'; import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js'; import XSidebar from '@/ui/_common_/navbar.vue'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import MkButton from '@/components/MkButton.vue'; -import { getScrollContainer } from '@/scripts/scroll.js'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; import { $i } from '@/account.js'; @@ -171,7 +170,7 @@ function showSettings() { os.pageWindow('/settings/deck'); } -let columnsEl = $shallowRef<HTMLElement>(); +const columnsEl = shallowRef<HTMLElement>(); const addColumn = async (ev) => { const columns = [ @@ -212,7 +211,7 @@ const onContextmenu = (ev) => { function onWheel(ev: WheelEvent) { if (ev.deltaX === 0) { - columnsEl.scrollLeft += ev.deltaY; + columnsEl.value.scrollLeft += ev.deltaY; } } @@ -236,7 +235,7 @@ function changeProfile(ev: MouseEvent) { deckStore.set('profile', k); unisonReload(); }, - }))), null, { + }))), { type: 'divider' }, { text: i18n.ts._deck.newProfile, icon: 'ph-plus ph-bold ph-lg', action: async () => { diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index d139d975fc..7cd1d6aee9 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, shallowRef } from 'vue'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; @@ -26,7 +26,7 @@ const props = defineProps<{ isStacked: boolean; }>(); -let timeline = $shallowRef<InstanceType<typeof MkTimeline>>(); +const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); onMounted(() => { if (props.column.antennaId == null) { diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index de6f336e09..95ed900f7d 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; @@ -32,8 +33,8 @@ const props = defineProps<{ isStacked: boolean; }>(); -let timeline = $shallowRef<InstanceType<typeof MkTimeline>>(); -let channel = $shallowRef<Misskey.entities.Channel>(); +const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); +const channel = shallowRef<Misskey.entities.Channel>(); if (props.column.channelId == null) { setChannel(); @@ -58,14 +59,14 @@ async function setChannel() { } async function post() { - if (!channel || channel.id !== props.column.channelId) { - channel = await os.api('channels/show', { + if (!channel.value || channel.value.id !== props.column.channelId) { + channel.value = await os.api('channels/show', { channelId: props.column.channelId, }); } os.post({ - channel, + channel: channel.value, }); } diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 09e91360b6..77e074f2ff 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onBeforeUnmount, onMounted, provide, watch } from 'vue'; +import { onBeforeUnmount, onMounted, provide, watch, shallowRef, ref, computed } from 'vue'; import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column } from './deck-store'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; @@ -67,16 +67,16 @@ const emit = defineEmits<{ (ev: 'headerWheel', ctx: WheelEvent): void; }>(); -let body = $shallowRef<HTMLDivElement | null>(); +const body = shallowRef<HTMLDivElement | null>(); -let dragging = $ref(false); -watch($$(dragging), v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd')); +const dragging = ref(false); +watch(dragging, v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd')); -let draghover = $ref(false); -let dropready = $ref(false); +const draghover = ref(false); +const dropready = ref(false); -const isMainColumn = $computed(() => props.column.type === 'main'); -const active = $computed(() => props.column.active !== false); +const isMainColumn = computed(() => props.column.type === 'main'); +const active = computed(() => props.column.active !== false); onMounted(() => { os.deckGlobalEvents.on('column.dragStart', onOtherDragStart); @@ -89,11 +89,11 @@ onBeforeUnmount(() => { }); function onOtherDragStart() { - dropready = true; + dropready.value = true; } function onOtherDragEnd() { - dropready = false; + dropready.value = false; } function toggleActive() { @@ -104,7 +104,7 @@ function toggleActive() { } function getMenu() { - let items = [{ + let items: MenuItem[] = [{ icon: 'ph-gear ph-bold ph-lg', text: i18n.ts._deck.configureColumn, action: async () => { @@ -170,7 +170,7 @@ function getMenu() { action: () => { popRightColumn(props.column.id); }, - } : undefined, null, { + } : undefined, { type: 'divider' }, { icon: 'ph-trash ph-bold ph-lg', text: i18n.ts.remove, danger: true, @@ -180,7 +180,7 @@ function getMenu() { }]; if (props.menu) { - items.unshift(null); + items.unshift({ type: 'divider' }); items = props.menu.concat(items); } @@ -208,8 +208,8 @@ function onContextmenu(ev: MouseEvent) { } function goTop() { - if (body) { - body.scrollTo({ + if (body.value) { + body.value.scrollTo({ top: 0, behavior: 'smooth', }); @@ -223,17 +223,17 @@ function onDragstart(ev) { // Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう // SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately window.setTimeout(() => { - dragging = true; + dragging.value = true; }, 10); } function onDragend(ev) { - dragging = false; + dragging.value = false; } function onDragover(ev) { // 自分自身がドラッグされている場合 - if (dragging) { + if (dragging.value) { // 自分自身にはドロップさせない ev.dataTransfer.dropEffect = 'none'; } else { @@ -241,16 +241,16 @@ function onDragover(ev) { ev.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; - if (isDeckColumn) draghover = true; + if (isDeckColumn) draghover.value = true; } } function onDragleave() { - draghover = false; + draghover.value = false; } function onDrop(ev) { - draghover = false; + draghover.value = false; os.deckGlobalEvents.emit('column.dragEnd'); const id = ev.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_); diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue index bbdf1b86e9..e2a212be46 100644 --- a/packages/frontend/src/ui/deck/direct-column.vue +++ b/packages/frontend/src/ui/deck/direct-column.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import XColumn from './column.vue'; import { Column } from './deck-store.js'; import MkNotes from '@/components/MkNotes.vue'; @@ -30,11 +30,11 @@ const pagination = { }, }; -const tlComponent: InstanceType<typeof MkNotes> = $ref(); +const tlComponent = ref<InstanceType<typeof MkNotes>>(); function reloadTimeline() { return new Promise<void>((res) => { - tlComponent.pagingComponent?.reload().then(() => { + tlComponent.value.pagingComponent?.reload().then(() => { res(); }); }); diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index 0e1f7d62bd..3e0ec6aac0 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { watch, shallowRef, ref } from 'vue'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store'; import MkTimeline from '@/components/MkTimeline.vue'; @@ -26,14 +26,14 @@ const props = defineProps<{ isStacked: boolean; }>(); -let timeline = $shallowRef<InstanceType<typeof MkTimeline>>(); -const withRenotes = $ref(props.column.withRenotes ?? true); +const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); +const withRenotes = ref(props.column.withRenotes ?? true); if (props.column.listId == null) { setList(); } -watch($$(withRenotes), v => { +watch(withRenotes, v => { updateColumn(props.column.id, { withRenotes: v, }); @@ -72,7 +72,7 @@ const menu = [ { type: 'switch', text: i18n.ts.showRenotes, - ref: $$(withRenotes), + ref: withRenotes, }, ]; </script> diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index 40639d46aa..4e17cb80fd 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ComputedRef, provide, shallowRef } from 'vue'; +import { ComputedRef, provide, shallowRef, ref } from 'vue'; import XColumn from './column.vue'; import { deckStore, Column } from '@/ui/deck/deck-store.js'; import * as os from '@/os.js'; @@ -35,11 +35,11 @@ defineProps<{ }>(); const contents = shallowRef<HTMLElement>(); -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); +const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); provide('router', mainRouter); provideMetadataReceiver((info) => { - pageMetadata = info; + pageMetadata.value = info; }); /* diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue index bb9b174cb7..7df07fd8d7 100644 --- a/packages/frontend/src/ui/deck/mentions-column.vue +++ b/packages/frontend/src/ui/deck/mentions-column.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import XColumn from './column.vue'; import { Column } from './deck-store.js'; import MkNotes from '@/components/MkNotes.vue'; @@ -22,11 +22,11 @@ defineProps<{ isStacked: boolean; }>(); -const tlComponent: InstanceType<typeof MkNotes> = $ref(); +const tlComponent = ref<InstanceType<typeof MkNotes>>(); function reloadTimeline() { return new Promise<void>((res) => { - tlComponent.pagingComponent?.reload().then(() => { + tlComponent.value.pagingComponent?.reload().then(() => { res(); }); }); diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue index ce1b26e36a..6a28bab091 100644 --- a/packages/frontend/src/ui/deck/notifications-column.vue +++ b/packages/frontend/src/ui/deck/notifications-column.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; +import { defineAsyncComponent, shallowRef } from 'vue'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import XNotifications from '@/components/MkNotifications.vue'; @@ -24,7 +24,7 @@ const props = defineProps<{ isStacked: boolean; }>(); -let notificationsComponent = $shallowRef<InstanceType<typeof XNotifications>>(); +const notificationsComponent = shallowRef<InstanceType<typeof XNotifications>>(); function func() { os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), { diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index b66c3ba618..5fbd1389b7 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, shallowRef } from 'vue'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; @@ -26,7 +26,7 @@ const props = defineProps<{ isStacked: boolean; }>(); -let timeline = $shallowRef<InstanceType<typeof MkTimeline>>(); +const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); onMounted(() => { if (props.column.roleId == null) { diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 5237010f50..f6167b08f9 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, watch } from 'vue'; +import { onMounted, watch, ref, shallowRef } from 'vue'; import XColumn from './column.vue'; import { removeColumn, updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; @@ -48,29 +48,29 @@ const props = defineProps<{ isStacked: boolean; }>(); -let disabled = $ref(false); -let timeline = $shallowRef<InstanceType<typeof MkTimeline>>(); +const disabled = ref(false); +const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); const isBubbleTimelineAvailable = ($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable); -const withRenotes = $ref(props.column.withRenotes ?? true); -const withReplies = $ref(props.column.withReplies ?? false); -const onlyFiles = $ref(props.column.onlyFiles ?? false); +const withRenotes = ref(props.column.withRenotes ?? true); +const withReplies = ref(props.column.withReplies ?? false); +const onlyFiles = ref(props.column.onlyFiles ?? false); -watch($$(withRenotes), v => { +watch(withRenotes, v => { updateColumn(props.column.id, { withRenotes: v, }); }); -watch($$(withReplies), v => { +watch(withReplies, v => { updateColumn(props.column.id, { withReplies: v, }); }); -watch($$(onlyFiles), v => { +watch(onlyFiles, v => { updateColumn(props.column.id, { onlyFiles: v, }); @@ -80,7 +80,7 @@ onMounted(() => { if (props.column.tl == null) { setType(); } else if ($i) { - disabled = ( + disabled.value = ( (!((instance.policies.ltlAvailable) || ($i.policies.ltlAvailable)) && ['local', 'social'].includes(props.column.tl)) || (!((instance.policies.gtlAvailable) || ($i.policies.gtlAvailable)) && ['global'].includes(props.column.tl)) || (!((instance.policies.btlAvailable) || ($i.policies.btlAvailable)) && ['bubble'].includes(props.column.tl))); @@ -120,15 +120,17 @@ const menu = [{ }, { type: 'switch', text: i18n.ts.showRenotes, - ref: $$(withRenotes), + ref: withRenotes, }, props.column.tl === 'local' || props.column.tl === 'social' ? { type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, - ref: $$(withReplies), + ref: withReplies, + disabled: onlyFiles, } : undefined, { type: 'switch', text: i18n.ts.fileAttachedOnly, - ref: $$(onlyFiles), + ref: onlyFiles, + disabled: props.column.tl === 'local' || props.column.tl === 'social' ? withReplies : false, }]; </script> diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue index 52d0c2a825..d111f92443 100644 --- a/packages/frontend/src/ui/deck/widgets-column.vue +++ b/packages/frontend/src/ui/deck/widgets-column.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { ref } from 'vue'; import XColumn from './column.vue'; import { addColumnWidget, Column, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store.js'; import XWidgets from '@/components/MkWidgets.vue'; @@ -26,7 +26,7 @@ const props = defineProps<{ isStacked: boolean; }>(); -let edit = $ref(false); +const edit = ref(false); function addWidget(widget) { addColumnWidget(props.column.id, widget); @@ -45,7 +45,7 @@ function updateWidgets(widgets) { } function func() { - edit = !edit; + edit.value = !edit.value; } const menu = [{ diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue index cc5433f81c..f32f2de3df 100644 --- a/packages/frontend/src/ui/minimum.vue +++ b/packages/frontend/src/ui/minimum.vue @@ -14,19 +14,19 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { provide, ComputedRef } from 'vue'; +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'; -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); +const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); provide('router', mainRouter); provideMetadataReceiver((info) => { - pageMetadata = info; - if (pageMetadata.value) { - document.title = `${pageMetadata.value.title} | ${instanceName}`; + pageMetadata.value = info; + if (pageMetadata.value.value) { + document.title = `${pageMetadata.value.value.title} | ${instanceName}`; } }); diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 448f2f81f8..1d8e26bfcc 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;" @contextmenu.stop="onContextmenu"> <template #header> <div> - <XAnnouncements v-if="$i" :class="$style.announcements"/> + <XAnnouncements v-if="$i"/> <XStatusBars :class="$style.statusbars"/> </div> </template> @@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.spacer"></div> </MkStickyContainer> - <div v-if="isDesktop" :class="$style.widgets"> + <div v-if="isDesktop && !pageMetadata?.needWideArea" :class="$style.widgets"> <XWidgets/> </div> - <button v-if="!isDesktop && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ph-squares-four ph-bold ph-lg"></i></button> + <button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ph-squares-four ph-bold ph-lg"></i></button> <div v-if="isMobile" ref="navFooter" :class="$style.nav"> <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ph-list ph-bold ph-lg-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button> @@ -95,7 +95,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, provide, onMounted, computed, ref, ComputedRef, watch, shallowRef, Ref } from 'vue'; +import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef, Ref } from 'vue'; import XCommon from './_common_/common.vue'; import type MkStickyContainer from '@/components/global/MkStickyContainer.vue'; import { instanceName } from '@/config.js'; @@ -127,14 +127,14 @@ window.addEventListener('resize', () => { isMobile.value = deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD; }); -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); -const widgetsShowing = $ref(false); -const navFooter = $shallowRef<HTMLElement>(); +const pageMetadata = ref<null | PageMetadata>(); +const widgetsShowing = ref(false); +const navFooter = shallowRef<HTMLElement>(); const contents = shallowRef<InstanceType<typeof MkStickyContainer>>(); provide('router', mainRouter); provideMetadataReceiver((info) => { - pageMetadata = info; + pageMetadata.value = info.value; if (pageMetadata.value) { document.title = `${pageMetadata.value.title} | ${instanceName}`; } @@ -216,16 +216,16 @@ function top() { }); } -let navFooterHeight = $ref(0); -provide<Ref<number>>(CURRENT_STICKY_BOTTOM, $$(navFooterHeight)); +const navFooterHeight = ref(0); +provide<Ref<number>>(CURRENT_STICKY_BOTTOM, navFooterHeight); -watch($$(navFooter), () => { - if (navFooter) { - navFooterHeight = navFooter.offsetHeight; - document.body.style.setProperty('--stickyBottom', `${navFooterHeight}px`); +watch(navFooter, () => { + if (navFooter.value) { + navFooterHeight.value = navFooter.value.offsetHeight; + document.body.style.setProperty('--stickyBottom', `${navFooterHeight.value}px`); document.body.style.setProperty('--minBottomSpacing', 'var(--minBottomSpacingMobile)'); } else { - navFooterHeight = 0; + navFooterHeight.value = 0; document.body.style.setProperty('--stickyBottom', '0px'); document.body.style.setProperty('--minBottomSpacing', '0px'); } diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue index 08ed6d324c..57d6ae0330 100644 --- a/packages/frontend/src/ui/universal.widgets.vue +++ b/packages/frontend/src/ui/universal.widgets.vue @@ -13,10 +13,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts"> -let editMode = $ref(false); +import { computed, ref } from 'vue'; +const editMode = ref(false); </script> <script lang="ts" setup> -import { } from 'vue'; import XWidgets from '@/components/MkWidgets.vue'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; @@ -30,7 +30,7 @@ const props = withDefaults(defineProps<{ place: null, }); -const widgets = $computed(() => { +const widgets = computed(() => { if (props.place === null) return defaultStore.reactiveState.widgets.value; if (props.place === 'left') return defaultStore.reactiveState.widgets.value.filter(w => w.place === 'left'); return defaultStore.reactiveState.widgets.value.filter(w => w.place !== 'left'); diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index 8d795e6dcb..459ba946b6 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -67,9 +67,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ComputedRef, onMounted, provide } from 'vue'; +import { ComputedRef, onMounted, provide, ref, computed } from 'vue'; import XCommon from './_common_/common.vue'; -import { host, instanceName } from '@/config.js'; +import { instanceName } from '@/config.js'; import * as os from '@/os.js'; import { instance } from '@/instance.js'; import XSigninDialog from '@/components/MkSigninDialog.vue'; @@ -82,13 +82,13 @@ import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue'; const DESKTOP_THRESHOLD = 1100; -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); +const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); provide('router', mainRouter); provideMetadataReceiver((info) => { - pageMetadata = info; - if (pageMetadata.value) { - document.title = `${pageMetadata.value.title} | ${instanceName}`; + pageMetadata.value = info; + if (pageMetadata.value.value) { + document.title = `${pageMetadata.value.value.title} | ${instanceName}`; } }); @@ -97,14 +97,14 @@ const announcements = { limit: 10, }; -const isTimelineAvailable = $ref(instance.policies?.ltlAvailable || instance.policies?.gtlAvailable); +const isTimelineAvailable = ref(instance.policies?.ltlAvailable || instance.policies?.gtlAvailable); -let showMenu = $ref(false); -let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD); -let narrow = $ref(window.innerWidth < 1280); -let meta = $ref(); +const showMenu = ref(false); +const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); +const narrow = ref(window.innerWidth < 1280); +const meta = ref(); -const keymap = $computed(() => { +const keymap = computed(() => { return { 'd': () => { if (ColdDeviceStorage.get('syncDeviceDarkMode')) return; @@ -116,10 +116,10 @@ const keymap = $computed(() => { }; }); -const root = $computed(() => mainRouter.currentRoute.value.name === 'index'); +const root = computed(() => mainRouter.currentRoute.value.name === 'index'); os.api('meta', { detail: true }).then(res => { - meta = res; + meta.value = res; }); function signin() { @@ -135,15 +135,15 @@ function signup() { } onMounted(() => { - if (!isDesktop) { + if (!isDesktop.value) { window.addEventListener('resize', () => { - if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop = true; + if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true; }, { passive: true }); } }); defineExpose({ - showMenu: $$(showMenu), + showMenu: showMenu, }); </script> diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue index a4f75136ec..9f92f78764 100644 --- a/packages/frontend/src/ui/zen.vue +++ b/packages/frontend/src/ui/zen.vue @@ -22,22 +22,22 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { provide, ComputedRef } from 'vue'; +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'; -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); +const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); const showBottom = !(new URLSearchParams(location.search)).has('zen') && ui === 'deck'; provide('router', mainRouter); provideMetadataReceiver((info) => { - pageMetadata = info; - if (pageMetadata.value) { - document.title = `${pageMetadata.value.title} | ${instanceName}`; + pageMetadata.value = info; + if (pageMetadata.value.value) { + document.title = `${pageMetadata.value.value.title} | ${instanceName}`; } }); diff --git a/packages/frontend/src/unicode-emoji-indexes/en-US.json b/packages/frontend/src/unicode-emoji-indexes/en-US.json index 567125c4c7..4d8b040ad2 100644 --- a/packages/frontend/src/unicode-emoji-indexes/en-US.json +++ b/packages/frontend/src/unicode-emoji-indexes/en-US.json @@ -103,6 +103,7 @@ "🫥": ["depressed", "disappear", "hide", "introvert", "invisible", "tensen"], "🫤": ["disappointed", "meh", "skeptical", "unsure"], "🥹": ["angry", "cry", "proud", "resist", "sad"], + "🫨": ["earthquake", "face", "shaking", "shock", "vibrate"], "💩": ["hankey", "shitface", "fail", "turd", "shit"], "😈": ["devil", "horns"], "👿": ["devil", "angry", "horns"], @@ -132,6 +133,8 @@ "✊": ["fingers", "hand", "grasp"], "🤛": ["hand", "fistbump"], "🤜": ["hand", "fistbump"], + "🫷": ["hand", "high_five", "leftward", "push", "refuse", "stop", "wait"], + "🫸": ["hand", "high_five", "push", "refuse", "rightward", "stop", "wait"], "✌": ["fingers", "ohyeah", "hand", "peace", "victory", "two"], "👌": ["fingers", "limbs", "perfect", "ok", "okay"], "✋": ["fingers", "stop", "highfive", "palm", "ban"], @@ -453,6 +456,7 @@ "🐸": ["animal", "nature", "croak", "toad"], "🦑": ["animal", "nature", "ocean", "sea"], "🐙": ["animal", "creature", "ocean", "sea", "nature", "beach"], + "🪼": ["animal", "creature", "ocean", "sea", "nature", "beach"], "🦐": ["animal", "ocean", "nature", "seafood"], "🐵": ["animal", "nature", "circus"], "🦍": ["animal", "nature", "circus"], @@ -466,7 +470,9 @@ "🐤": ["animal", "chicken", "bird"], "🐣": ["animal", "chicken", "egg", "born", "baby", "bird"], "🐥": ["animal", "chicken", "baby", "bird"], + "🪿": ["animal", "nature", "bird", "fowl", "goose", "honk", "silly"], "🦆": ["animal", "nature", "bird", "mallard"], + "🐦⬛": ["animal", "nature", "bird", "black", "crow", "raven", "rook"], "🦅": ["animal", "nature", "bird"], "🦉": ["animal", "nature", "bird", "hoot"], "🦇": ["animal", "nature", "blind", "vampire"], @@ -474,9 +480,11 @@ "🐗": ["animal", "nature"], "🐴": ["animal", "brown", "nature"], "🦄": ["animal", "nature", "mystical"], + "🫎": ["animal", "nature", "antlers", "elk", "mammal"], "🐝": ["animal", "insect", "nature", "bug", "spring", "honey"], "🐛": ["animal", "insect", "nature", "worm"], "🦋": ["animal", "insect", "nature", "caterpillar"], + "🫏": ["animal", "ass", "burro", "mammal", "mule", "stubborn"], "🐌": ["slow", "animal", "shell"], "🐞": ["animal", "insect", "nature", "ladybug"], "🐜": ["animal", "insect", "nature", "bug"], @@ -546,6 +554,7 @@ "🐻❄️": ["animal", "nature"], "🦤": ["animal", "nature"], "🪶": ["animal", "nature"], + "🪽": ["angelic", "aviation", "bird", "flying", "mythology"], "🦭": ["animal", "nature"], "🐾": ["animal", "tracking", "footprints", "dog", "cat", "pet", "feet"], "🐉": ["animal", "myth", "nature", "chinese", "green"], @@ -576,6 +585,7 @@ "🌻": ["nature", "plant", "fall"], "🌹": ["flowers", "valentines", "love", "spring"], "🥀": ["plant", "nature", "flower"], + "🪻": ["plant", "nature", "flower", "bluebonnet", "lavender", "lupine", "snapdragon"], "🌷": ["flowers", "plant", "nature", "summer", "spring"], "🌼": ["nature", "flowers", "yellow"], "🌸": ["nature", "plant", "spring", "flower"], @@ -655,6 +665,7 @@ "🥝": ["fruit", "food"], "🥭": ["fruit", "food", "tropical"], "🥑": ["fruit", "food"], + "🫛": ["beans", "edamame", "legume", "pea", "pod", "vegetable", "food"], "🥦": ["fruit", "food", "vegetable"], "🍅": ["fruit", "vegetable", "nature", "food"], "🍆": ["vegetable", "nature", "food", "aubergine"], @@ -668,6 +679,7 @@ "🌽": ["food", "vegetable", "plant"], "🥬": ["food", "vegetable", "plant", "bok choy", "cabbage", "kale", "lettuce"], "🍠": ["food", "nature"], + "🫚": ["food", "nature", "beer", "root", "spice"], "🥜": ["food", "nut"], "🧄": ["food"], "🧅": ["food"], @@ -850,9 +862,11 @@ "🎧": ["music", "score", "gadgets"], "🎼": ["treble", "clef", "compose"], "🎹": ["piano", "instrument", "compose"], + "🪇": ["instrument", "music", "percussion", "rattle", "shake"], "🥁": ["music", "instrument", "drumsticks", "snare"], "🎷": ["music", "instrument", "jazz", "blues"], "🎺": ["music", "brass"], + "🪈": ["music", "fife", "pipe", "recorder", "woodwind"], "🎸": ["music", "instrument"], "🎻": ["music", "instrument", "orchestra", "symphony"], "🪕": ["music", "instrument"], @@ -1108,6 +1122,7 @@ "🩹": ["health", "hospital", "medicine", "needle", "doctor", "nurse"], "🩺": ["health", "hospital", "medicine", "needle", "doctor", "nurse"], "🪒": ["health"], + "🪮": ["afro", "comb", "hair", "pick"], "🩻": [], "🩼": [], "🧬": ["biologist", "genetics", "life"], @@ -1156,6 +1171,7 @@ "🎊": ["festival", "party", "birthday", "circus"], "🎉": ["party", "congratulations", "birthday", "magic", "circus", "celebration"], "🎎": ["japanese", "toy", "kimono"], + "🪭": ["cooling", "dance", "fan", "flutter", "hot", "shy"], "🎐": ["nature", "ding", "spring", "bell"], "🎌": ["japanese", "nation", "country", "border"], "🏮": ["light", "paper", "halloween", "spooky"], @@ -1237,14 +1253,17 @@ "🪧": [], "💯": ["score", "perfect", "numbers", "century", "exam", "quiz", "test", "pass", "hundred"], "🔢": ["numbers", "blue-square"], + "🩷": ["love", "like", "affection", "valentines"], "❤️": ["love", "like", "affection", "valentines"], "🧡": ["love", "like", "affection", "valentines"], "💛": ["love", "like", "affection", "valentines"], "💚": ["love", "like", "affection", "valentines"], + "🩵": ["love", "like", "affection", "valentines"], "💙": ["love", "like", "affection", "valentines"], "💜": ["love", "like", "affection", "valentines"], "🤎": ["love", "like", "affection", "valentines"], "🖤": ["love", "like", "affection", "valentines"], + "🩶": ["love", "like", "affection", "valentines"], "🤍": ["love", "like", "affection", "valentines"], "💔": ["sad", "sorry", "break", "heart", "heartbreak"], "❣": ["decoration", "love"], @@ -1263,6 +1282,7 @@ "☪": ["islam"], "🕉": ["hinduism", "buddhism", "sikhism", "jainism"], "☸": ["hinduism", "buddhism", "sikhism", "jainism"], + "🪯": ["religion", "sikh"], "✡": ["judaism"], "🔯": ["purple-square", "religion", "jewish", "hexagram"], "🕎": ["hanukkah", "candles", "jewish"], @@ -1358,6 +1378,7 @@ "🛃": ["passport", "border", "blue-square"], "🛄": ["blue-square", "airport", "transport"], "🛅": ["blue-square", "travel"], + "🛜": ["blue-square", "computer", "internet", "network"], "♿": ["blue-square", "disabled", "a11y", "accessibility"], "🚭": ["cigarette", "blue-square", "smell", "smoke"], "🚾": ["toilet", "restroom", "blue-square"], @@ -1527,258 +1548,258 @@ "🕥": ["time", "late", "early", "schedule"], "🕦": ["time", "late", "early", "schedule"], "🕧": ["time", "late", "early", "schedule"], - "🇦🇫": ["af", "flag", "nation", "country", "banner"], - "🇦🇽": ["Åland", "islands", "flag", "nation", "country", "banner"], - "🇦🇱": ["al", "flag", "nation", "country", "banner"], - "🇩🇿": ["dz", "flag", "nation", "country", "banner"], - "🇦🇸": ["american", "ws", "flag", "nation", "country", "banner"], + "🇦🇫": ["af", "afghanistan", "flag", "nation", "country", "banner"], + "🇦🇽": ["ax", "Åland", "aland", "islands", "flag", "nation", "country", "banner"], + "🇦🇱": ["al", "albania", "flag", "nation", "country", "banner"], + "🇩🇿": ["dz", "algeria", "flag", "nation", "country", "banner"], + "🇦🇸": ["as", "american", "samoa", "flag", "nation", "country", "banner"], "🇦🇩": ["ad", "flag", "nation", "country", "banner"], - "🇦🇴": ["ao", "flag", "nation", "country", "banner"], - "🇦🇮": ["ai", "flag", "nation", "country", "banner"], - "🇦🇶": ["aq", "flag", "nation", "country", "banner"], - "🇦🇬": ["antigua", "barbuda", "flag", "nation", "country", "banner"], - "🇦🇷": ["ar", "flag", "nation", "country", "banner"], - "🇦🇲": ["am", "flag", "nation", "country", "banner"], - "🇦🇼": ["aw", "flag", "nation", "country", "banner"], - "🇦🇨": ["flag", "nation", "country", "banner"], - "🇦🇺": ["au", "flag", "nation", "country", "banner"], - "🇦🇹": ["at", "flag", "nation", "country", "banner"], - "🇦🇿": ["az", "flag", "nation", "country", "banner"], - "🇧🇸": ["bs", "flag", "nation", "country", "banner"], - "🇧🇭": ["bh", "flag", "nation", "country", "banner"], - "🇧🇩": ["bd", "flag", "nation", "country", "banner"], - "🇧🇧": ["bb", "flag", "nation", "country", "banner"], - "🇧🇾": ["by", "flag", "nation", "country", "banner"], - "🇧🇪": ["be", "flag", "nation", "country", "banner"], - "🇧🇿": ["bz", "flag", "nation", "country", "banner"], - "🇧🇯": ["bj", "flag", "nation", "country", "banner"], - "🇧🇲": ["bm", "flag", "nation", "country", "banner"], - "🇧🇹": ["bt", "flag", "nation", "country", "banner"], - "🇧🇴": ["bo", "flag", "nation", "country", "banner"], - "🇧🇶": ["bonaire", "flag", "nation", "country", "banner"], - "🇧🇦": ["bosnia", "herzegovina", "flag", "nation", "country", "banner"], - "🇧🇼": ["bw", "flag", "nation", "country", "banner"], - "🇧🇷": ["br", "flag", "nation", "country", "banner"], - "🇮🇴": ["british", "indian", "ocean", "territory", "flag", "nation", "country", "banner"], - "🇻🇬": ["british", "virgin", "islands", "bvi", "flag", "nation", "country", "banner"], - "🇧🇳": ["bn", "darussalam", "flag", "nation", "country", "banner"], - "🇧🇬": ["bg", "flag", "nation", "country", "banner"], - "🇧🇫": ["burkina", "faso", "flag", "nation", "country", "banner"], - "🇧🇮": ["bi", "flag", "nation", "country", "banner"], - "🇨🇻": ["cabo", "verde", "flag", "nation", "country", "banner"], - "🇰🇭": ["kh", "flag", "nation", "country", "banner"], - "🇨🇲": ["cm", "flag", "nation", "country", "banner"], - "🇨🇦": ["ca", "flag", "nation", "country", "banner"], - "🇮🇨": ["canary", "islands", "flag", "nation", "country", "banner"], - "🇰🇾": ["cayman", "islands", "flag", "nation", "country", "banner"], - "🇨🇫": ["central", "african", "republic", "flag", "nation", "country", "banner"], - "🇹🇩": ["td", "flag", "nation", "country", "banner"], - "🇨🇱": ["flag", "nation", "country", "banner"], - "🇨🇳": ["china", "chinese", "prc", "flag", "country", "nation", "banner"], - "🇨🇽": ["christmas", "island", "flag", "nation", "country", "banner"], - "🇨🇨": ["cocos", "keeling", "islands", "flag", "nation", "country", "banner"], - "🇨🇴": ["co", "flag", "nation", "country", "banner"], - "🇰🇲": ["km", "flag", "nation", "country", "banner"], - "🇨🇬": ["congo", "flag", "nation", "country", "banner"], - "🇨🇩": ["congo", "democratic", "republic", "flag", "nation", "country", "banner"], - "🇨🇰": ["cook", "islands", "flag", "nation", "country", "banner"], - "🇨🇷": ["costa", "rica", "flag", "nation", "country", "banner"], - "🇭🇷": ["hr", "flag", "nation", "country", "banner"], - "🇨🇺": ["cu", "flag", "nation", "country", "banner"], - "🇨🇼": ["curaçao", "flag", "nation", "country", "banner"], - "🇨🇾": ["cy", "flag", "nation", "country", "banner"], - "🇨🇿": ["cz", "flag", "nation", "country", "banner"], - "🇩🇰": ["dk", "flag", "nation", "country", "banner"], - "🇩🇯": ["dj", "flag", "nation", "country", "banner"], - "🇩🇲": ["dm", "flag", "nation", "country", "banner"], - "🇩🇴": ["dominican", "republic", "flag", "nation", "country", "banner"], - "🇪🇨": ["ec", "flag", "nation", "country", "banner"], - "🇪🇬": ["eg", "flag", "nation", "country", "banner"], - "🇸🇻": ["el", "salvador", "flag", "nation", "country", "banner"], - "🇬🇶": ["equatorial", "gn", "flag", "nation", "country", "banner"], - "🇪🇷": ["er", "flag", "nation", "country", "banner"], - "🇪🇪": ["ee", "flag", "nation", "country", "banner"], - "🇪🇹": ["et", "flag", "nation", "country", "banner"], - "🇪🇺": ["european", "union", "flag", "banner"], - "🇫🇰": ["falkland", "islands", "malvinas", "flag", "nation", "country", "banner"], - "🇫🇴": ["faroe", "islands", "flag", "nation", "country", "banner"], - "🇫🇯": ["fj", "flag", "nation", "country", "banner"], - "🇫🇮": ["fi", "flag", "nation", "country", "banner"], - "🇫🇷": ["banner", "flag", "nation", "france", "french", "country"], - "🇬🇫": ["french", "guiana", "flag", "nation", "country", "banner"], - "🇵🇫": ["french", "polynesia", "flag", "nation", "country", "banner"], - "🇹🇫": ["french", "southern", "territories", "flag", "nation", "country", "banner"], - "🇬🇦": ["ga", "flag", "nation", "country", "banner"], - "🇬🇲": ["gm", "flag", "nation", "country", "banner"], - "🇬🇪": ["ge", "flag", "nation", "country", "banner"], - "🇩🇪": ["german", "nation", "flag", "country", "banner"], - "🇬🇭": ["gh", "flag", "nation", "country", "banner"], - "🇬🇮": ["gi", "flag", "nation", "country", "banner"], - "🇬🇷": ["gr", "flag", "nation", "country", "banner"], - "🇬🇱": ["gl", "flag", "nation", "country", "banner"], - "🇬🇩": ["gd", "flag", "nation", "country", "banner"], - "🇬🇵": ["gp", "flag", "nation", "country", "banner"], - "🇬🇺": ["gu", "flag", "nation", "country", "banner"], - "🇬🇹": ["gt", "flag", "nation", "country", "banner"], - "🇬🇬": ["gg", "flag", "nation", "country", "banner"], - "🇬🇳": ["gn", "flag", "nation", "country", "banner"], - "🇬🇼": ["gw", "bissau", "flag", "nation", "country", "banner"], - "🇬🇾": ["gy", "flag", "nation", "country", "banner"], - "🇭🇹": ["ht", "flag", "nation", "country", "banner"], - "🇭🇳": ["hn", "flag", "nation", "country", "banner"], - "🇭🇰": ["hong", "kong", "flag", "nation", "country", "banner"], - "🇭🇺": ["hu", "flag", "nation", "country", "banner"], - "🇮🇸": ["is", "flag", "nation", "country", "banner"], - "🇮🇳": ["in", "flag", "nation", "country", "banner"], - "🇮🇩": ["flag", "nation", "country", "banner"], - "🇮🇷": ["iran, ", "islamic", "republic", "flag", "nation", "country", "banner"], - "🇮🇶": ["iq", "flag", "nation", "country", "banner"], - "🇮🇪": ["ie", "flag", "nation", "country", "banner"], - "🇮🇲": ["isle", "man", "flag", "nation", "country", "banner"], - "🇮🇱": ["il", "flag", "nation", "country", "banner"], - "🇮🇹": ["italy", "flag", "nation", "country", "banner"], - "🇨🇮": ["ivory", "coast", "flag", "nation", "country", "banner"], - "🇯🇲": ["jm", "flag", "nation", "country", "banner"], - "🇯🇵": ["japanese", "nation", "flag", "country", "banner"], - "🇯🇪": ["je", "flag", "nation", "country", "banner"], - "🇯🇴": ["jo", "flag", "nation", "country", "banner"], - "🇰🇿": ["kz", "flag", "nation", "country", "banner"], - "🇰🇪": ["ke", "flag", "nation", "country", "banner"], - "🇰🇮": ["ki", "flag", "nation", "country", "banner"], - "🇽🇰": ["xk", "flag", "nation", "country", "banner"], - "🇰🇼": ["kw", "flag", "nation", "country", "banner"], - "🇰🇬": ["kg", "flag", "nation", "country", "banner"], - "🇱🇦": ["lao", "democratic", "republic", "flag", "nation", "country", "banner"], - "🇱🇻": ["lv", "flag", "nation", "country", "banner"], - "🇱🇧": ["lb", "flag", "nation", "country", "banner"], - "🇱🇸": ["ls", "flag", "nation", "country", "banner"], - "🇱🇷": ["lr", "flag", "nation", "country", "banner"], - "🇱🇾": ["ly", "flag", "nation", "country", "banner"], - "🇱🇮": ["li", "flag", "nation", "country", "banner"], - "🇱🇹": ["lt", "flag", "nation", "country", "banner"], - "🇱🇺": ["lu", "flag", "nation", "country", "banner"], - "🇲🇴": ["macao", "flag", "nation", "country", "banner"], - "🇲🇰": ["macedonia, ", "flag", "nation", "country", "banner"], - "🇲🇬": ["mg", "flag", "nation", "country", "banner"], - "🇲🇼": ["mw", "flag", "nation", "country", "banner"], - "🇲🇾": ["my", "flag", "nation", "country", "banner"], - "🇲🇻": ["mv", "flag", "nation", "country", "banner"], - "🇲🇱": ["ml", "flag", "nation", "country", "banner"], - "🇲🇹": ["mt", "flag", "nation", "country", "banner"], - "🇲🇭": ["marshall", "islands", "flag", "nation", "country", "banner"], - "🇲🇶": ["mq", "flag", "nation", "country", "banner"], - "🇲🇷": ["mr", "flag", "nation", "country", "banner"], - "🇲🇺": ["mu", "flag", "nation", "country", "banner"], - "🇾🇹": ["yt", "flag", "nation", "country", "banner"], - "🇲🇽": ["mx", "flag", "nation", "country", "banner"], - "🇫🇲": ["micronesia, ", "federated", "states", "flag", "nation", "country", "banner"], - "🇲🇩": ["moldova, ", "republic", "flag", "nation", "country", "banner"], - "🇲🇨": ["mc", "flag", "nation", "country", "banner"], - "🇲🇳": ["mn", "flag", "nation", "country", "banner"], - "🇲🇪": ["me", "flag", "nation", "country", "banner"], - "🇲🇸": ["ms", "flag", "nation", "country", "banner"], - "🇲🇦": ["ma", "flag", "nation", "country", "banner"], - "🇲🇿": ["mz", "flag", "nation", "country", "banner"], - "🇲🇲": ["mm", "flag", "nation", "country", "banner"], - "🇳🇦": ["na", "flag", "nation", "country", "banner"], - "🇳🇷": ["nr", "flag", "nation", "country", "banner"], - "🇳🇵": ["np", "flag", "nation", "country", "banner"], - "🇳🇱": ["nl", "flag", "nation", "country", "banner"], - "🇳🇨": ["new", "caledonia", "flag", "nation", "country", "banner"], - "🇳🇿": ["new", "zealand", "flag", "nation", "country", "banner"], - "🇳🇮": ["ni", "flag", "nation", "country", "banner"], - "🇳🇪": ["ne", "flag", "nation", "country", "banner"], - "🇳🇬": ["flag", "nation", "country", "banner"], - "🇳🇺": ["nu", "flag", "nation", "country", "banner"], - "🇳🇫": ["norfolk", "island", "flag", "nation", "country", "banner"], - "🇲🇵": ["northern", "mariana", "islands", "flag", "nation", "country", "banner"], - "🇰🇵": ["north", "korea", "nation", "flag", "country", "banner"], - "🇳🇴": ["no", "flag", "nation", "country", "banner"], - "🇴🇲": ["om_symbol", "flag", "nation", "country", "banner"], - "🇵🇰": ["pk", "flag", "nation", "country", "banner"], - "🇵🇼": ["pw", "flag", "nation", "country", "banner"], - "🇵🇸": ["palestine", "palestinian", "territories", "flag", "nation", "country", "banner"], - "🇵🇦": ["pa", "flag", "nation", "country", "banner"], - "🇵🇬": ["papua", "new", "guinea", "flag", "nation", "country", "banner"], - "🇵🇾": ["py", "flag", "nation", "country", "banner"], - "🇵🇪": ["pe", "flag", "nation", "country", "banner"], - "🇵🇭": ["ph", "flag", "nation", "country", "banner"], - "🇵🇳": ["pitcairn", "flag", "nation", "country", "banner"], - "🇵🇱": ["pl", "flag", "nation", "country", "banner"], - "🇵🇹": ["pt", "flag", "nation", "country", "banner"], - "🇵🇷": ["puerto", "rico", "flag", "nation", "country", "banner"], - "🇶🇦": ["qa", "flag", "nation", "country", "banner"], - "🇷🇪": ["réunion", "flag", "nation", "country", "banner"], - "🇷🇴": ["ro", "flag", "nation", "country", "banner"], - "🇷🇺": ["russian", "federation", "flag", "nation", "country", "banner"], - "🇷🇼": ["rw", "flag", "nation", "country", "banner"], - "🇧🇱": ["saint", "barthélemy", "flag", "nation", "country", "banner"], - "🇸🇭": ["saint", "helena", "ascension", "tristan", "cunha", "flag", "nation", "country", "banner"], - "🇰🇳": ["saint", "kitts", "nevis", "flag", "nation", "country", "banner"], - "🇱🇨": ["saint", "lucia", "flag", "nation", "country", "banner"], - "🇵🇲": ["saint", "pierre", "miquelon", "flag", "nation", "country", "banner"], - "🇻🇨": ["saint", "vincent", "grenadines", "flag", "nation", "country", "banner"], - "🇼🇸": ["ws", "flag", "nation", "country", "banner"], - "🇸🇲": ["san", "marino", "flag", "nation", "country", "banner"], - "🇸🇹": ["sao", "tome", "principe", "flag", "nation", "country", "banner"], - "🇸🇦": ["flag", "nation", "country", "banner"], - "🇸🇳": ["sn", "flag", "nation", "country", "banner"], - "🇷🇸": ["rs", "flag", "nation", "country", "banner"], - "🇸🇨": ["sc", "flag", "nation", "country", "banner"], - "🇸🇱": ["sierra", "leone", "flag", "nation", "country", "banner"], - "🇸🇬": ["sg", "flag", "nation", "country", "banner"], - "🇸🇽": ["sint", "maarten", "dutch", "flag", "nation", "country", "banner"], - "🇸🇰": ["sk", "flag", "nation", "country", "banner"], - "🇸🇮": ["si", "flag", "nation", "country", "banner"], - "🇸🇧": ["solomon", "islands", "flag", "nation", "country", "banner"], - "🇸🇴": ["so", "flag", "nation", "country", "banner"], - "🇿🇦": ["south", "africa", "flag", "nation", "country", "banner"], - "🇬🇸": ["south", "georgia", "sandwich", "islands", "flag", "nation", "country", "banner"], - "🇰🇷": ["south", "korea", "nation", "flag", "country", "banner"], - "🇸🇸": ["south", "sd", "flag", "nation", "country", "banner"], - "🇪🇸": ["spain", "flag", "nation", "country", "banner"], - "🇱🇰": ["sri", "lanka", "flag", "nation", "country", "banner"], - "🇸🇩": ["sd", "flag", "nation", "country", "banner"], - "🇸🇷": ["sr", "flag", "nation", "country", "banner"], - "🇸🇿": ["sz", "flag", "nation", "country", "banner"], - "🇸🇪": ["se", "flag", "nation", "country", "banner"], - "🇨🇭": ["ch", "flag", "nation", "country", "banner"], - "🇸🇾": ["syrian", "arab", "republic", "flag", "nation", "country", "banner"], - "🇹🇼": ["tw", "flag", "nation", "country", "banner"], - "🇹🇯": ["tj", "flag", "nation", "country", "banner"], - "🇹🇿": ["tanzania, ", "united", "republic", "flag", "nation", "country", "banner"], - "🇹🇭": ["th", "flag", "nation", "country", "banner"], - "🇹🇱": ["timor", "leste", "flag", "nation", "country", "banner"], - "🇹🇬": ["tg", "flag", "nation", "country", "banner"], - "🇹🇰": ["tk", "flag", "nation", "country", "banner"], - "🇹🇴": ["to", "flag", "nation", "country", "banner"], - "🇹🇹": ["trinidad", "tobago", "flag", "nation", "country", "banner"], - "🇹🇦": ["flag", "nation", "country", "banner"], - "🇹🇳": ["tn", "flag", "nation", "country", "banner"], - "🇹🇷": ["turkey", "flag", "nation", "country", "banner"], - "🇹🇲": ["flag", "nation", "country", "banner"], - "🇹🇨": ["turks", "caicos", "islands", "flag", "nation", "country", "banner"], - "🇹🇻": ["flag", "nation", "country", "banner"], - "🇺🇬": ["ug", "flag", "nation", "country", "banner"], - "🇺🇦": ["ua", "flag", "nation", "country", "banner"], - "🇦🇪": ["united", "arab", "emirates", "flag", "nation", "country", "banner"], - "🇬🇧": ["united", "kingdom", "great", "britain", "northern", "ireland", "flag", "nation", "country", "banner", "british", "UK", "english", "england", "union jack"], + "🇦🇴": ["ao", "angola", "flag", "nation", "country", "banner"], + "🇦🇮": ["ai", "anguilla", "flag", "nation", "country", "banner"], + "🇦🇶": ["aq", "antarctique", "flag", "nation", "country", "banner"], + "🇦🇬": ["ag", "antigua", "barbuda", "flag", "nation", "country", "banner"], + "🇦🇷": ["ar", "argentina", "flag", "nation", "country", "banner"], + "🇦🇲": ["am", "armenia", "flag", "nation", "country", "banner"], + "🇦🇼": ["aw", "aruba", "flag", "nation", "country", "banner"], + "🇦🇨": ["ac", "ascension", "island", "flag", "nation", "country", "banner"], + "🇦🇺": ["au", "australia", "flag", "nation", "country", "banner"], + "🇦🇹": ["at", "austria", "flag", "nation", "country", "banner"], + "🇦🇿": ["az", "azerbaijan", "flag", "nation", "country", "banner"], + "🇧🇸": ["bs", "bahamas", "flag", "nation", "country", "banner"], + "🇧🇭": ["bh", "bahrain", "flag", "nation", "country", "banner"], + "🇧🇩": ["bd", "bangladesh", "flag", "nation", "country", "banner"], + "🇧🇧": ["bb", "barbados", "flag", "nation", "country", "banner"], + "🇧🇾": ["by", "belarus", "flag", "nation", "country", "banner"], + "🇧🇪": ["be", "belgium", "flag", "nation", "country", "banner"], + "🇧🇿": ["bz", "belize", "flag", "nation", "country", "banner"], + "🇧🇯": ["bj", "benin", "flag", "nation", "country", "banner"], + "🇧🇲": ["bm", "bermuda", "flag", "nation", "country", "banner"], + "🇧🇹": ["bt", "bhutan", "flag", "nation", "country", "banner"], + "🇧🇴": ["bo", "bolivia", "flag", "nation", "country", "banner"], + "🇧🇶": ["bq", "bonaire", "flag", "nation", "country", "banner"], + "🇧🇦": ["ba", "bosnia", "herzegovina", "flag", "nation", "country", "banner"], + "🇧🇼": ["bw", "botswana", "flag", "nation", "country", "banner"], + "🇧🇷": ["br", "brazil", "flag", "nation", "country", "banner"], + "🇮🇴": ["io", "british", "indian", "ocean", "territory", "flag", "nation", "country", "banner"], + "🇻🇬": ["vg", "british", "virgin", "islands", "bvi", "flag", "nation", "country", "banner"], + "🇧🇳": ["bn", "brunei", "darussalam", "flag", "nation", "country", "banner"], + "🇧🇬": ["bg", "bulgaria", "flag", "nation", "country", "banner"], + "🇧🇫": ["bf", "burkina", "faso", "flag", "nation", "country", "banner"], + "🇧🇮": ["bi", "burundi", "flag", "nation", "country", "banner"], + "🇨🇻": ["cv", "cabo", "verde", "flag", "nation", "country", "banner"], + "🇰🇭": ["kh", "cambodia", "flag", "nation", "country", "banner"], + "🇨🇲": ["cm", "cameroon", "flag", "nation", "country", "banner"], + "🇨🇦": ["ca", "canada", "flag", "nation", "country", "banner"], + "🇮🇨": ["ic", "canary", "islands", "flag", "nation", "country", "banner"], + "🇰🇾": ["ky", "cayman", "islands", "flag", "nation", "country", "banner"], + "🇨🇫": ["cf", "central", "african", "republic", "flag", "nation", "country", "banner"], + "🇹🇩": ["td", "chad", "flag", "nation", "country", "banner"], + "🇨🇱": ["cl", "chile", "flag", "nation", "country", "banner"], + "🇨🇳": ["cn", "china", "chinese", "prc", "flag", "country", "nation", "banner"], + "🇨🇽": ["cx", "christmas", "island", "flag", "nation", "country", "banner"], + "🇨🇨": ["cc", "cocos", "keeling", "islands", "flag", "nation", "country", "banner"], + "🇨🇴": ["co", "colombia", "flag", "nation", "country", "banner"], + "🇰🇲": ["km", "comoros", "flag", "nation", "country", "banner"], + "🇨🇬": ["cg", "republic", "congo", "flag", "nation", "country", "banner"], + "🇨🇩": ["cd", "democratic", "republic", "congo", "flag", "nation", "country", "banner"], + "🇨🇰": ["ck", "cook", "islands", "flag", "nation", "country", "banner"], + "🇨🇷": ["cr", "costa", "rica", "flag", "nation", "country", "banner"], + "🇭🇷": ["hr", "croatia", "flag", "nation", "country", "banner"], + "🇨🇺": ["cu", "cuba", "flag", "nation", "country", "banner"], + "🇨🇼": ["cw", "curacao", "curaçao", "flag", "nation", "country", "banner"], + "🇨🇾": ["cy", "cyprus", "flag", "nation", "country", "banner"], + "🇨🇿": ["cz", "czech", "republic", "flag", "nation", "country", "banner"], + "🇩🇰": ["dk", "denmark", "flag", "nation", "country", "banner"], + "🇩🇯": ["dj", "djibouti", "flag", "nation", "country", "banner"], + "🇩🇲": ["dm", "dominica", "flag", "nation", "country", "banner"], + "🇩🇴": ["do", "dominican", "republic", "flag", "nation", "country", "banner"], + "🇪🇨": ["ec", "ecuador", "flag", "nation", "country", "banner"], + "🇪🇬": ["eg", "egypt", "flag", "nation", "country", "banner"], + "🇸🇻": ["sv", "el", "salvador", "flag", "nation", "country", "banner"], + "🇬🇶": ["gq", "equatorial", "guinea", "flag", "nation", "country", "banner"], + "🇪🇷": ["er", "eritrea", "flag", "nation", "country", "banner"], + "🇪🇪": ["ee", "estonia", "flag", "nation", "country", "banner"], + "🇪🇹": ["et", "ethiopia", "flag", "nation", "country", "banner"], + "🇪🇺": ["eu", "european", "union", "flag", "banner"], + "🇫🇰": ["fk", "falkland", "islands", "malvinas", "flag", "nation", "country", "banner"], + "🇫🇴": ["fo", "faroe", "islands", "flag", "nation", "country", "banner"], + "🇫🇯": ["fj", "fiji", "flag", "nation", "country", "banner"], + "🇫🇮": ["fi", "finland", "flag", "nation", "country", "banner"], + "🇫🇷": ["fr", "banner", "flag", "nation", "france", "french", "country"], + "🇬🇫": ["gf", "french", "guiana", "flag", "nation", "country", "banner"], + "🇵🇫": ["pf", "french", "polynesia", "flag", "nation", "country", "banner"], + "🇹🇫": ["tf", "french", "southern", "territories", "flag", "nation", "country", "banner"], + "🇬🇦": ["ga", "gabon", "flag", "nation", "country", "banner"], + "🇬🇲": ["gm", "gambia", "flag", "nation", "country", "banner"], + "🇬🇪": ["ge", "georgia", "flag", "nation", "country", "banner"], + "🇩🇪": ["de", "deutschland", "german", "nation", "flag", "country", "banner"], + "🇬🇭": ["gh", "ghana", "flag", "nation", "country", "banner"], + "🇬🇮": ["gi", "gibraltar", "flag", "nation", "country", "banner"], + "🇬🇷": ["gr", "greece", "flag", "nation", "country", "banner"], + "🇬🇱": ["gl", "green", "land", "flag", "nation", "country", "banner"], + "🇬🇩": ["gd", "grenada", "flag", "nation", "country", "banner"], + "🇬🇵": ["gp", "guadeloupe", "flag", "nation", "country", "banner"], + "🇬🇺": ["gu", "guam", "flag", "nation", "country", "banner"], + "🇬🇹": ["gt", "guatemala", "flag", "nation", "country", "banner"], + "🇬🇬": ["gg", "guernsey", "flag", "nation", "country", "banner"], + "🇬🇳": ["gn", "guinea", "flag", "nation", "country", "banner"], + "🇬🇼": ["gw", "guiana", "bissau", "flag", "nation", "country", "banner"], + "🇬🇾": ["gy", "guyana", "flag", "nation", "country", "banner"], + "🇭🇹": ["ht", "haiti", "flag", "nation", "country", "banner"], + "🇭🇳": ["hn", "honduras", "flag", "nation", "country", "banner"], + "🇭🇰": ["hk", "hong", "kong", "flag", "nation", "country", "banner"], + "🇭🇺": ["hu", "hungary", "flag", "nation", "country", "banner"], + "🇮🇸": ["is", "iceland", "Ísland", "flag", "nation", "country", "banner"], + "🇮🇳": ["in", "india", "flag", "nation", "country", "banner"], + "🇮🇩": ["id", "indonesia", "flag", "nation", "country", "banner"], + "🇮🇷": ["ir", "iran", "islamic", "republic", "flag", "nation", "country", "banner"], + "🇮🇶": ["iq", "iraq", "flag", "nation", "country", "banner"], + "🇮🇪": ["ie", "ireland", "flag", "nation", "country", "banner"], + "🇮🇲": ["im", "isle", "man", "flag", "nation", "country", "banner"], + "🇮🇱": ["il", "israel", "flag", "nation", "country", "banner"], + "🇮🇹": ["it", "italy", "flag", "nation", "country", "banner"], + "🇨🇮": ["ci", "cote", "divoire", "Côte", "d'Ivoire", "ivory", "coast", "flag", "nation", "country", "banner"], + "🇯🇲": ["jm", "jamaica", "flag", "nation", "country", "banner"], + "🇯🇵": ["jp", "japan", "japanese", "nation", "flag", "country", "banner"], + "🇯🇪": ["je", "jersey", "flag", "nation", "country", "banner"], + "🇯🇴": ["jo", "jordan", "flag", "nation", "country", "banner"], + "🇰🇿": ["kz", "kazakhstan", "flag", "nation", "country", "banner"], + "🇰🇪": ["ke", "kenya", "flag", "nation", "country", "banner"], + "🇰🇮": ["ki", "kiribati", "flag", "nation", "country", "banner"], + "🇽🇰": ["xk", "kosovo", "flag", "nation", "country", "banner"], + "🇰🇼": ["kw", "kuwait", "flag", "nation", "country", "banner"], + "🇰🇬": ["kg", "kyrgyzstan", "kyrgyz", "flag", "nation", "country", "banner"], + "🇱🇦": ["la", "laos", "lao", "democratic", "republic", "flag", "nation", "country", "banner"], + "🇱🇻": ["lv", "latvia", "flag", "nation", "country", "banner"], + "🇱🇧": ["lb", "lebanon", "flag", "nation", "country", "banner"], + "🇱🇸": ["ls", "lesotho", "flag", "nation", "country", "banner"], + "🇱🇷": ["lr", "liberia", "flag", "nation", "country", "banner"], + "🇱🇾": ["ly", "libya", "flag", "nation", "country", "banner"], + "🇱🇮": ["li", "liechtenstein", "flag", "nation", "country", "banner"], + "🇱🇹": ["lt", "lithuania", "flag", "nation", "country", "banner"], + "🇱🇺": ["lu", "luxembourg", "flag", "nation", "country", "banner"], + "🇲🇴": ["mo", "macao", "macau", "flag", "nation", "country", "banner"], + "🇲🇰": ["mk", "north", "macedonia", "flag", "nation", "country", "banner"], + "🇲🇬": ["mg", "madagascar", "flag", "nation", "country", "banner"], + "🇲🇼": ["mw", "malawi", "flag", "nation", "country", "banner"], + "🇲🇾": ["my", "malaysia", "flag", "nation", "country", "banner"], + "🇲🇻": ["mv", "maldives", "republic", "flag", "nation", "country", "banner"], + "🇲🇱": ["ml", "mali", "flag", "nation", "country", "banner"], + "🇲🇹": ["mt", "malta", "flag", "nation", "country", "banner"], + "🇲🇭": ["mh", "marshall", "islands", "flag", "nation", "country", "banner"], + "🇲🇶": ["mq", "martinique", "flag", "nation", "country", "banner"], + "🇲🇷": ["mr", "mauritania", "flag", "nation", "country", "banner"], + "🇲🇺": ["mu", "mauritius", "flag", "nation", "country", "banner"], + "🇾🇹": ["yt", "mayotte", "flag", "nation", "country", "banner"], + "🇲🇽": ["mx", "mexico", "flag", "nation", "country", "banner"], + "🇫🇲": ["fm", "micronesia", "federated", "states", "flag", "nation", "country", "banner"], + "🇲🇩": ["md", "moldova", "republic", "flag", "nation", "country", "banner"], + "🇲🇨": ["mc", "monaco", "flag", "nation", "country", "banner"], + "🇲🇳": ["mn", "mongolia", "flag", "nation", "country", "banner"], + "🇲🇪": ["me", "montenegro", "flag", "nation", "country", "banner"], + "🇲🇸": ["ms", "montserrat", "flag", "nation", "country", "banner"], + "🇲🇦": ["ma", "morocco", "flag", "nation", "country", "banner"], + "🇲🇿": ["mz", "mozambique", "flag", "nation", "country", "banner"], + "🇲🇲": ["mm", "myanmar", "flag", "nation", "country", "banner"], + "🇳🇦": ["na", "namibia", "flag", "nation", "country", "banner"], + "🇳🇷": ["nr", "nauru", "flag", "nation", "country", "banner"], + "🇳🇵": ["np", "nepal", "flag", "nation", "country", "banner"], + "🇳🇱": ["nl", "netherlands", "flag", "nation", "country", "banner"], + "🇳🇨": ["nc", "new", "caledonia", "flag", "nation", "country", "banner"], + "🇳🇿": ["nz", "new", "zealand", "flag", "nation", "country", "banner"], + "🇳🇮": ["ni", "nicaragua", "flag", "nation", "country", "banner"], + "🇳🇪": ["ne", "niger", "flag", "nation", "country", "banner"], + "🇳🇬": ["ng", "nigeria", "flag", "nation", "country", "banner"], + "🇳🇺": ["nu", "niue", "flag", "nation", "country", "banner"], + "🇳🇫": ["nf", "norfolk", "island", "flag", "nation", "country", "banner"], + "🇲🇵": ["mp", "northern", "mariana", "islands", "flag", "nation", "country", "banner"], + "🇰🇵": ["kp", "democratic", "people", "republic", "north", "korea", "nation", "flag", "country", "banner"], + "🇳🇴": ["no", "norway", "flag", "nation", "country", "banner"], + "🇴🇲": ["om", "oman", "flag", "nation", "country", "banner"], + "🇵🇰": ["pk", "pakistan", "flag", "nation", "country", "banner"], + "🇵🇼": ["pw", "palau", "flag", "nation", "country", "banner"], + "🇵🇸": ["ps", "palestine", "palestinian", "territories", "flag", "nation", "country", "banner"], + "🇵🇦": ["pa", "panama", "flag", "nation", "country", "banner"], + "🇵🇬": ["pg", "papua", "new", "guinea", "flag", "nation", "country", "banner"], + "🇵🇾": ["py", "paraguay", "flag", "nation", "country", "banner"], + "🇵🇪": ["pe", "peru", "flag", "nation", "country", "banner"], + "🇵🇭": ["ph", "philippines", "flag", "nation", "country", "banner"], + "🇵🇳": ["pn", "pitcairn", "flag", "nation", "country", "banner"], + "🇵🇱": ["pl", "poland", "flag", "nation", "country", "banner"], + "🇵🇹": ["pt", "portugal", "flag", "nation", "country", "banner"], + "🇵🇷": ["pr", "puerto", "rico", "flag", "nation", "country", "banner"], + "🇶🇦": ["qa", "qatar", "flag", "nation", "country", "banner"], + "🇷🇪": ["re", "reunion", "réunion", "flag", "nation", "country", "banner"], + "🇷🇴": ["ro", "romania", "flag", "nation", "country", "banner"], + "🇷🇺": ["ru", "russian", "federation", "flag", "nation", "country", "banner"], + "🇷🇼": ["rw", "rwanda", "flag", "nation", "country", "banner"], + "🇧🇱": ["bl", "saint", "barthélemy", "flag", "nation", "country", "banner"], + "🇸🇭": ["sh", "saint", "helena", "ascension", "tristan", "cunha", "flag", "nation", "country", "banner"], + "🇰🇳": ["kn", "saint", "kitts", "nevis", "flag", "nation", "country", "banner"], + "🇱🇨": ["lc", "saint", "lucia", "flag", "nation", "country", "banner"], + "🇵🇲": ["pm", "saint", "pierre", "miquelon", "flag", "nation", "country", "banner"], + "🇻🇨": ["vc", "saint", "vincent", "grenadines", "flag", "nation", "country", "banner"], + "🇼🇸": ["ws", "western", "samoa", "flag", "nation", "country", "banner"], + "🇸🇲": ["sm", "san", "marino", "flag", "nation", "country", "banner"], + "🇸🇹": ["st", "sao", "tome", "principe", "flag", "nation", "country", "banner"], + "🇸🇦": ["saudi", "arabia", "flag", "nation", "country", "banner"], + "🇸🇳": ["sn", "senegal", "flag", "nation", "country", "banner"], + "🇷🇸": ["rs", "serbia", "flag", "nation", "country", "banner"], + "🇸🇨": ["sc", "seychelles", "flag", "nation", "country", "banner"], + "🇸🇱": ["sl", "sierra", "leone", "flag", "nation", "country", "banner"], + "🇸🇬": ["sg", "singapore", "flag", "nation", "country", "banner"], + "🇸🇽": ["sx", "sint", "maarten", "dutch", "flag", "nation", "country", "banner"], + "🇸🇰": ["sk", "slovakia", "flag", "nation", "country", "banner"], + "🇸🇮": ["si", "slovenia", "flag", "nation", "country", "banner"], + "🇸🇧": ["sb", "solomon", "islands", "flag", "nation", "country", "banner"], + "🇸🇴": ["so", "somalia", "flag", "nation", "country", "banner"], + "🇿🇦": ["za", "south", "africa", "flag", "nation", "country", "banner"], + "🇬🇸": ["gs", "south", "georgia", "sandwich", "islands", "flag", "nation", "country", "banner"], + "🇰🇷": ["kr", "south", "korea", "nation", "flag", "country", "banner"], + "🇸🇸": ["ss", "south", "sudan", "flag", "nation", "country", "banner"], + "🇪🇸": ["es", "spain", "españa", "flag", "nation", "country", "banner"], + "🇱🇰": ["lk", "sri", "lanka", "flag", "nation", "country", "banner"], + "🇸🇩": ["sd", "sudan", "flag", "nation", "country", "banner"], + "🇸🇷": ["sr", "suriname", "flag", "nation", "country", "banner"], + "🇸🇿": ["sz", "eswatini", "flag", "nation", "country", "banner"], + "🇸🇪": ["se", "sweden", "flag", "nation", "country", "banner"], + "🇨🇭": ["ch", "switzerland", "confoederatio", "helvetica", "flag", "nation", "country", "banner"], + "🇸🇾": ["sy", "syrian", "arab", "republic", "flag", "nation", "country", "banner"], + "🇹🇼": ["tw", "taiwan", "flag", "nation", "country", "banner"], + "🇹🇯": ["tj", "tajikistan", "flag", "nation", "country", "banner"], + "🇹🇿": ["tz", "tanzania", "united", "republic", "flag", "nation", "country", "banner"], + "🇹🇭": ["th", "thailand", "flag", "nation", "country", "banner"], + "🇹🇱": ["tl", "timor", "leste", "flag", "nation", "country", "banner"], + "🇹🇬": ["tg", "togo", "flag", "nation", "country", "banner"], + "🇹🇰": ["tk", "tokelau", "flag", "nation", "country", "banner"], + "🇹🇴": ["to", "tonga", "flag", "nation", "country", "banner"], + "🇹🇹": ["tt", "trinidad", "tobago", "flag", "nation", "country", "banner"], + "🇹🇦": ["ta", "tristan", "da", "cunha", "flag", "nation", "country", "banner"], + "🇹🇳": ["tn", "tunisia", "flag", "nation", "country", "banner"], + "🇹🇷": ["tr", "turkey", "türkiye", "flag", "nation", "country", "banner"], + "🇹🇲": ["tm", "turkmenistan", "flag", "nation", "country", "banner"], + "🇹🇨": ["tc", "turks", "caicos", "islands", "flag", "nation", "country", "banner"], + "🇹🇻": ["tv", "tuvalu", "flag", "nation", "country", "banner"], + "🇺🇬": ["ug", "uganda", "flag", "nation", "country", "banner"], + "🇺🇦": ["ua", "ukraine", "flag", "nation", "country", "banner"], + "🇦🇪": ["ae", "united", "arab", "emirates", "flag", "nation", "country", "banner"], + "🇬🇧": ["gb", "united", "kingdom", "great", "britain", "northern", "ireland", "flag", "nation", "country", "banner", "british", "uk", "english", "england", "union jack"], "🏴": ["flag", "english"], "🏴": ["flag", "scottish"], "🏴": ["flag", "welsh"], - "🇺🇸": ["united", "states", "america", "flag", "nation", "country", "banner"], - "🇻🇮": ["virgin", "islands", "us", "flag", "nation", "country", "banner"], - "🇺🇾": ["uy", "flag", "nation", "country", "banner"], - "🇺🇿": ["uz", "flag", "nation", "country", "banner"], - "🇻🇺": ["vu", "flag", "nation", "country", "banner"], - "🇻🇦": ["vatican", "city", "flag", "nation", "country", "banner"], - "🇻🇪": ["ve", "bolivarian", "republic", "flag", "nation", "country", "banner"], - "🇻🇳": ["viet", "nam", "flag", "nation", "country", "banner"], - "🇼🇫": ["wallis", "futuna", "flag", "nation", "country", "banner"], - "🇪🇭": ["western", "sahara", "flag", "nation", "country", "banner"], - "🇾🇪": ["ye", "flag", "nation", "country", "banner"], - "🇿🇲": ["zm", "flag", "nation", "country", "banner"], - "🇿🇼": ["zw", "flag", "nation", "country", "banner"], - "🇺🇳": ["un", "flag", "banner"], + "🇺🇸": ["us", "usa", "united", "states", "america", "flag", "nation", "country", "banner"], + "🇻🇮": ["vi", "virgin", "islands", "us", "flag", "nation", "country", "banner"], + "🇺🇾": ["uy", "uruguay", "flag", "nation", "country", "banner"], + "🇺🇿": ["uz", "uzbekistan", "flag", "nation", "country", "banner"], + "🇻🇺": ["vu", "vanuatu", "flag", "nation", "country", "banner"], + "🇻🇦": ["va", "vatican", "city", "flag", "nation", "country", "banner"], + "🇻🇪": ["ve", "venezuela", "flag", "nation", "country", "banner"], + "🇻🇳": ["vn", "viet", "nam", "flag", "nation", "country", "banner"], + "🇼🇫": ["wf", "wallis", "futuna", "flag", "nation", "country", "banner"], + "🇪🇭": ["eh", "western", "sahara", "flag", "nation", "country", "banner"], + "🇾🇪": ["ye", "yemen", "flag", "nation", "country", "banner"], + "🇿🇲": ["zm", "zambia", "flag", "nation", "country", "banner"], + "🇿🇼": ["zw", "zimbabwe", "flag", "nation", "country", "banner"], + "🇺🇳": ["un", "united", "nation", "flag", "banner"], "🏴☠️": ["skull", "crossbones", "flag", "banner"] } diff --git a/packages/frontend/src/widgets/WidgetActivity.chart.vue b/packages/frontend/src/widgets/WidgetActivity.chart.vue index 9cfd845ace..a207071324 100644 --- a/packages/frontend/src/widgets/WidgetActivity.chart.vue +++ b/packages/frontend/src/widgets/WidgetActivity.chart.vue @@ -34,18 +34,19 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref } from 'vue'; const props = defineProps<{ activity: any[] }>(); -let viewBoxX: number = $ref(147); -let viewBoxY: number = $ref(60); -let zoom: number = $ref(1); -let pos: number = $ref(0); -let pointsNote: any = $ref(null); -let pointsReply: any = $ref(null); -let pointsRenote: any = $ref(null); -let pointsTotal: any = $ref(null); +const viewBoxX = ref(147); +const viewBoxY = ref(60); +const zoom = ref(1); +const pos = ref(0); +const pointsNote = ref<any>(null); +const pointsReply = ref<any>(null); +const pointsRenote = ref<any>(null); +const pointsTotal = ref<any>(null); function dragListen(fn) { window.addEventListener('mousemove', fn); @@ -62,17 +63,17 @@ function dragClear(fn) { function onMousedown(ev) { const clickX = ev.clientX; const clickY = ev.clientY; - const baseZoom = zoom; - const basePos = pos; + const baseZoom = zoom.value; + const basePos = pos.value; // 動かした時 dragListen(me => { let moveLeft = me.clientX - clickX; let moveTop = me.clientY - clickY; - zoom = Math.max(1, baseZoom + (-moveTop / 20)); - pos = Math.min(0, basePos + moveLeft); - if (pos < -(((props.activity.length - 1) * zoom) - viewBoxX)) pos = -(((props.activity.length - 1) * zoom) - viewBoxX); + zoom.value = Math.max(1, baseZoom + (-moveTop / 20)); + pos.value = Math.min(0, basePos + moveLeft); + if (pos.value < -(((props.activity.length - 1) * zoom.value) - viewBoxX.value)) pos.value = -(((props.activity.length - 1) * zoom.value) - viewBoxX.value); render(); }); @@ -82,10 +83,10 @@ function render() { const peak = Math.max(...props.activity.map(d => d.total)); if (peak !== 0) { const activity = props.activity.slice().reverse(); - pointsNote = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.notes / peak)) * viewBoxY}`).join(' '); - pointsReply = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.replies / peak)) * viewBoxY}`).join(' '); - pointsRenote = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.renotes / peak)) * viewBoxY}`).join(' '); - pointsTotal = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.total / peak)) * viewBoxY}`).join(' '); + pointsNote.value = activity.map((d, i) => `${(i * zoom.value) + pos.value},${(1 - (d.notes / peak)) * viewBoxY.value}`).join(' '); + pointsReply.value = activity.map((d, i) => `${(i * zoom.value) + pos.value},${(1 - (d.replies / peak)) * viewBoxY.value}`).join(' '); + pointsRenote.value = activity.map((d, i) => `${(i * zoom.value) + pos.value},${(1 - (d.renotes / peak)) * viewBoxY.value}`).join(' '); + pointsTotal.value = activity.map((d, i) => `${(i * zoom.value) + pos.value},${(1 - (d.total / peak)) * viewBoxY.value}`).join(' '); } } </script> diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue index 788a9f2c7b..b107a47d8c 100644 --- a/packages/frontend/src/widgets/WidgetActivity.vue +++ b/packages/frontend/src/widgets/WidgetActivity.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import XCalendar from './WidgetActivity.calendar.vue'; import XChart from './WidgetActivity.chart.vue'; import { GetFormResultType } from '@/scripts/form.js'; diff --git a/packages/frontend/src/widgets/WidgetAichan.vue b/packages/frontend/src/widgets/WidgetAichan.vue index 76b35f6fed..fef026244c 100644 --- a/packages/frontend/src/widgets/WidgetAichan.vue +++ b/packages/frontend/src/widgets/WidgetAichan.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, shallowRef } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; const name = 'ai'; @@ -72,5 +72,6 @@ defineExpose<WidgetComponentExpose>({ height: 350px; border: none; pointer-events: none; + color-scheme: light; } </style> diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue index 8fb4ebe0f2..c17e9728a5 100644 --- a/packages/frontend/src/widgets/WidgetAiscript.vue +++ b/packages/frontend/src/widgets/WidgetAiscript.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import { Interpreter, Parser, utils } from '@syuilo/aiscript'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import * as os from '@/os.js'; import MkContainer from '@/components/MkContainer.vue'; diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue index 53b6020ffc..10248a840a 100644 --- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue +++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, Ref, ref, watch } from 'vue'; import { Interpreter, Parser } from '@syuilo/aiscript'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +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'; @@ -52,7 +52,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name, const parser = new Parser(); const root = ref<AsUiRoot>(); -const components: Ref<AsUiComponent>[] = $ref([]); +const components = ref<Ref<AsUiComponent>[]>([]); async function run() { const aiscript = new Interpreter({ @@ -60,7 +60,7 @@ async function run() { storageKey: 'widget', token: $i?.token, }), - ...registerAsUiLib(components, (_root) => { + ...registerAsUiLib(components.value, (_root) => { root.value = _root.value; }), }, { diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue new file mode 100644 index 0000000000..7c4455516d --- /dev/null +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -0,0 +1,127 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkContainer :showHeader="widgetProps.showHeader" class="mkw-bdayfollowings"> + <template #icon><i class="ti ti-cake"></i></template> + <template #header>{{ i18n.ts._widgets.birthdayFollowings }}</template> + + <div :class="$style.bdayFRoot"> + <MkLoading v-if="fetching"/> + <div v-else-if="users.length > 0" :class="$style.bdayFGrid"> + <MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar> + </div> + <div v-else :class="$style.bdayFFallback"> + <img :src="infoImageUrl" class="_ghost" :class="$style.bdayFFallbackImage"/> + <div>{{ i18n.ts.nothing }}</div> + </div> + </div> +</MkContainer> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +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 { useInterval } from '@/scripts/use-interval.js'; +import { i18n } from '@/i18n.js'; +import { infoImageUrl } from '@/instance.js'; +import { $i } from '@/account.js'; + +const name = i18n.ts._widgets.birthdayFollowings; + +const widgetPropsDef = { + showHeader: { + type: 'boolean' as const, + default: true, + }, +}; + +type WidgetProps = GetFormResultType<typeof widgetPropsDef>; + +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); + +const { widgetProps, configure } = useWidgetPropsManager(name, + widgetPropsDef, + props, + emit, +); + +const users = ref<Misskey.entities.FollowingFolloweePopulated[]>([]); +const fetching = ref(true); +let lastFetchedAt = '1970-01-01'; + +const fetch = () => { + if (!$i) { + users.value = []; + fetching.value = false; + return; + } + + const lfAtD = new Date(lastFetchedAt); + lfAtD.setHours(0, 0, 0, 0); + const now = new Date(); + now.setHours(0, 0, 0, 0); + + if (now > lfAtD) { + os.api('users/following', { + limit: 18, + birthday: now.toISOString(), + userId: $i.id, + }).then(res => { + users.value = res; + fetching.value = false; + }); + + lastFetchedAt = now.toISOString(); + } +}; + +useInterval(fetch, 1000 * 60, { + immediate: true, + afterMounted: true, +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, +}); +</script> + +<style lang="scss" module> +.bdayFRoot { + overflow: hidden; + min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--margin) * 2)); +} +.bdayFGrid { + display: grid; + grid-template-columns: repeat(6, 42px); + grid-template-rows: repeat(3, 42px); + place-content: center; + gap: 8px; + margin: var(--margin) auto; +} + +.bdayFFallback { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.bdayFFallbackImage { + height: 96px; + width: auto; + max-width: 90%; + margin-bottom: 8px; + border-radius: var(--radius); +} +</style> diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue index a7bdd4c49c..11082c1e3f 100644 --- a/packages/frontend/src/widgets/WidgetButton.vue +++ b/packages/frontend/src/widgets/WidgetButton.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { Interpreter, Parser } from '@syuilo/aiscript'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +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'; diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue index 0ad166c6ba..b3f814a0a7 100644 --- a/packages/frontend/src/widgets/WidgetCalendar.vue +++ b/packages/frontend/src/widgets/WidgetCalendar.vue @@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import { i18n } from '@/i18n.js'; import { useInterval } from '@/scripts/use-interval.js'; diff --git a/packages/frontend/src/widgets/WidgetClicker.vue b/packages/frontend/src/widgets/WidgetClicker.vue index 2b3b391165..aa49269017 100644 --- a/packages/frontend/src/widgets/WidgetClicker.vue +++ b/packages/frontend/src/widgets/WidgetClicker.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkClickerGame from '@/components/MkClickerGame.vue'; diff --git a/packages/frontend/src/widgets/WidgetClock.vue b/packages/frontend/src/widgets/WidgetClock.vue index e4ea2c97dd..22f053db59 100644 --- a/packages/frontend/src/widgets/WidgetClock.vue +++ b/packages/frontend/src/widgets/WidgetClock.vue @@ -29,8 +29,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { computed } from 'vue'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkAnalogClock from '@/components/MkAnalogClock.vue'; @@ -134,15 +134,15 @@ const { widgetProps, configure } = useWidgetPropsManager(name, emit, ); -const tzAbbrev = $computed(() => (widgetProps.timezone === null +const tzAbbrev = computed(() => (widgetProps.timezone === null ? timezones.find((tz) => tz.name.toLowerCase() === Intl.DateTimeFormat().resolvedOptions().timeZone.toLowerCase())?.abbrev : timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.abbrev) ?? '?'); -const tzOffset = $computed(() => widgetProps.timezone === null +const tzOffset = computed(() => widgetProps.timezone === null ? 0 - new Date().getTimezoneOffset() : timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.offset ?? 0); -const tzOffsetLabel = $computed(() => (tzOffset >= 0 ? '+' : '-') + Math.floor(tzOffset / 60).toString().padStart(2, '0') + ':' + (tzOffset % 60).toString().padStart(2, '0')); +const tzOffsetLabel = computed(() => (tzOffset.value >= 0 ? '+' : '-') + Math.floor(tzOffset.value / 60).toString().padStart(2, '0') + ':' + (tzOffset.value % 60).toString().padStart(2, '0')); defineExpose<WidgetComponentExpose>({ name, diff --git a/packages/frontend/src/widgets/WidgetDigitalClock.vue b/packages/frontend/src/widgets/WidgetDigitalClock.vue index 9ff5f8dcef..a4b90c49d3 100644 --- a/packages/frontend/src/widgets/WidgetDigitalClock.vue +++ b/packages/frontend/src/widgets/WidgetDigitalClock.vue @@ -14,7 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { computed } from 'vue'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import { timezones } from '@/scripts/timezones.js'; import MkDigitalClock from '@/components/MkDigitalClock.vue'; @@ -63,15 +64,15 @@ const { widgetProps, configure } = useWidgetPropsManager(name, emit, ); -const tzAbbrev = $computed(() => (widgetProps.timezone === null +const tzAbbrev = computed(() => (widgetProps.timezone === null ? timezones.find((tz) => tz.name.toLowerCase() === Intl.DateTimeFormat().resolvedOptions().timeZone.toLowerCase())?.abbrev : timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.abbrev) ?? '?'); -const tzOffset = $computed(() => widgetProps.timezone === null +const tzOffset = computed(() => widgetProps.timezone === null ? 0 - new Date().getTimezoneOffset() : timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.offset ?? 0); -const tzOffsetLabel = $computed(() => (tzOffset >= 0 ? '+' : '-') + Math.floor(tzOffset / 60).toString().padStart(2, '0') + ':' + (tzOffset % 60).toString().padStart(2, '0')); +const tzOffsetLabel = computed(() => (tzOffset.value >= 0 ? '+' : '-') + Math.floor(tzOffset.value / 60).toString().padStart(2, '0') + ':' + (tzOffset.value % 60).toString().padStart(2, '0')); defineExpose<WidgetComponentExpose>({ name, diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index e6ee590c15..605c24aaa3 100644 --- a/packages/frontend/src/widgets/WidgetFederation.vue +++ b/packages/frontend/src/widgets/WidgetFederation.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue index 4ae77e86fc..0fc96c0d35 100644 --- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue +++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue @@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { shallowRef } from 'vue'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkTagCloud from '@/components/MkTagCloud.vue'; @@ -47,8 +47,8 @@ const { widgetProps, configure } = useWidgetPropsManager(name, emit, ); -let cloud = $shallowRef<InstanceType<typeof MkTagCloud> | null>(); -let activeInstances = $shallowRef(null); +const cloud = shallowRef<InstanceType<typeof MkTagCloud> | null>(); +const activeInstances = shallowRef(null); function onInstanceClick(i) { os.pageWindow(`/instance-info/${i.host}`); @@ -59,8 +59,8 @@ useInterval(() => { sort: '+latestRequestReceivedAt', limit: 25, }).then(res => { - activeInstances = res; - if (cloud) cloud.update(); + activeInstances.value = res; + if (cloud.value) cloud.value.update(); }); }, 1000 * 60 * 3, { immediate: true, diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue index 3fa811fa13..2133deb363 100644 --- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue +++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import { host } from '@/config.js'; import { instance } from '@/instance.js'; diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index 09c27bfdec..c54682bb87 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -51,13 +51,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onUnmounted, reactive } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +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 * as sound from '@/scripts/sound.js'; import { deepClone } from '@/scripts/clone.js'; +import { defaultStore } from '@/store.js'; const name = 'jobQueue'; @@ -99,7 +100,18 @@ const current = reactive({ }, }); const prev = reactive({} as typeof current); -const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1); +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 => { + if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer'); + jammedAudioBuffer.value = buf; + }); +} for (const domain of ['inbox', 'deliver']) { prev[domain] = deepClone(current[domain]); @@ -113,8 +125,13 @@ const onStats = (stats) => { current[domain].waiting = stats[domain].waiting; current[domain].delayed = stats[domain].delayed; - if (current[domain].waiting > 0 && widgetProps.sound && jammedSound.paused) { - jammedSound.play(); + if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer.value && !jammedSoundNodePlaying.value) { + const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1); + if (soundNode) { + jammedSoundNodePlaying.value = true; + soundNode.onended = () => jammedSoundNodePlaying.value = false; + soundNode.start(); + } } } }; diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue index c2d5ef566c..8e9e67ade5 100644 --- a/packages/frontend/src/widgets/WidgetMemo.vue +++ b/packages/frontend/src/widgets/WidgetMemo.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, watch } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/widgets/WidgetNotifications.vue b/packages/frontend/src/widgets/WidgetNotifications.vue index 6e38b8308c..e858741aa1 100644 --- a/packages/frontend/src/widgets/WidgetNotifications.vue +++ b/packages/frontend/src/widgets/WidgetNotifications.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import XNotifications from '@/components/MkNotifications.vue'; diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue index 46fe991f37..0a6fec7f2e 100644 --- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue +++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import * as os from '@/os.js'; import { useInterval } from '@/scripts/use-interval.js'; diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue index c07f2dd262..808f023174 100644 --- a/packages/frontend/src/widgets/WidgetPhotos.vue +++ b/packages/frontend/src/widgets/WidgetPhotos.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onUnmounted, ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import { useStream } from '@/stream.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; diff --git a/packages/frontend/src/widgets/WidgetPostForm.vue b/packages/frontend/src/widgets/WidgetPostForm.vue index 320b47a4ff..9979ae256e 100644 --- a/packages/frontend/src/widgets/WidgetPostForm.vue +++ b/packages/frontend/src/widgets/WidgetPostForm.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkPostForm from '@/components/MkPostForm.vue'; diff --git a/packages/frontend/src/widgets/WidgetProfile.vue b/packages/frontend/src/widgets/WidgetProfile.vue index fc54af2d71..3ff57bab86 100644 --- a/packages/frontend/src/widgets/WidgetProfile.vue +++ b/packages/frontend/src/widgets/WidgetProfile.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import { $i } from '@/account.js'; import { userPage } from '@/filters/user.js'; diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index 1ada9f4be8..a718548731 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, watch, computed } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { url as base } from '@/config.js'; @@ -72,7 +72,7 @@ const fetchEndpoint = computed(() => { url.searchParams.set('url', widgetProps.url); return url; }); -let intervalClear = $ref<(() => void) | undefined>(); +const intervalClear = ref<(() => void) | undefined>(); const tick = () => { if (document.visibilityState === 'hidden' && rawItems.value.length !== 0) return; @@ -87,10 +87,10 @@ const tick = () => { watch(() => fetchEndpoint, tick); watch(() => widgetProps.refreshIntervalSec, () => { - if (intervalClear) { - intervalClear(); + if (intervalClear.value) { + intervalClear.value(); } - intervalClear = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), { + intervalClear.value = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), { immediate: true, afterMounted: true, }); diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index 790f94f7c3..607bb2f0ab 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, watch, computed } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import MarqueeText from '@/components/MkMarquee.vue'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; @@ -101,9 +101,9 @@ const fetchEndpoint = computed(() => { url.searchParams.set('url', widgetProps.url); return url; }); -let intervalClear = $ref<(() => void) | undefined>(); +const intervalClear = ref<(() => void) | undefined>(); -let key = $ref(0); +const key = ref(0); const tick = () => { if (document.visibilityState === 'hidden' && rawItems.value.length !== 0) return; @@ -113,16 +113,16 @@ const tick = () => { .then(feed => { rawItems.value = feed.items ?? []; fetching.value = false; - key++; + key.value++; }); }; watch(() => fetchEndpoint, tick); watch(() => widgetProps.refreshIntervalSec, () => { - if (intervalClear) { - intervalClear(); + if (intervalClear.value) { + intervalClear.value(); } - intervalClear = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), { + intervalClear.value = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), { immediate: true, afterMounted: true, }); diff --git a/packages/frontend/src/widgets/WidgetSearch.vue b/packages/frontend/src/widgets/WidgetSearch.vue index 979341e1ae..c114707b23 100644 --- a/packages/frontend/src/widgets/WidgetSearch.vue +++ b/packages/frontend/src/widgets/WidgetSearch.vue @@ -15,8 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, onMounted } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { defineAsyncComponent, ref } from 'vue'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import MkInput from '@/components/MkInput.vue'; import MkContainer from '@/components/MkContainer.vue'; import { i18n } from '@/i18n.js'; @@ -54,14 +54,12 @@ function onInputKeydown(evt: KeyboardEvent) { const router = useRouter(); -let key = $ref(0); -let searchQuery = $ref(''); -let notePagination = $ref(); -let searchOrigin = $ref('combined'); -let user = $ref(null); -let isLocalOnly = $ref(false); -let order = $ref(true); -let filetype = $ref(null); +let key = ref(0); +let searchQuery = ref(''); +let notePagination = ref(); +let isLocalOnly = ref(false); +let order = ref(true); +let filetype = ref<null | string>(null); function options(ev) { os.popupMenu([{ @@ -74,7 +72,7 @@ function options(ev) { icon: 'ph-image ph-bold ph-lg', text: 'With Images', action: () => { - filetype = 'image'; + filetype.value = 'image'; }, }, { @@ -82,7 +80,7 @@ function options(ev) { icon: 'ph-music-notes-simple ph-bold ph-lg', text: 'With Audios', action: () => { - filetype = 'audio'; + filetype.value = 'audio'; }, }, { @@ -90,20 +88,14 @@ function options(ev) { icon: 'ph-video ph-bold ph-lg', text: 'With Videos', action: () => { - filetype = 'video'; + filetype.value = 'video'; }, }], }], ev.currentTarget ?? ev.target); } -function selectUser() { - os.selectUser().then(_user => { - user = _user; - }); -} - async function search() { - const query = searchQuery.toString().trim(); + const query = searchQuery.value.toString().trim(); if (query == null || query === '') return; @@ -125,24 +117,24 @@ async function search() { return; } - notePagination = { + notePagination.value = { endpoint: 'notes/search', limit: 10, params: { query: searchQuery, - userId: user ? user.id : null, - order: order ? 'desc' : 'asc', - filetype: filetype, + userId: null, + order: order.value ? 'desc' : 'asc', + filetype: filetype.value, }, }; - if (isLocalOnly) notePagination.params.host = '.'; + if (isLocalOnly.value) notePagination.value.params.host = '.'; - key++; + key.value++; os.popup(defineAsyncComponent(() => import('@/components/SkSearchResultWindow.vue')), { - noteKey: key, - notePagination: notePagination, + noteKey: key.value, + notePagination: notePagination.value, }, { }, 'closed'); } diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue index 82b6246add..eccb9a00bf 100644 --- a/packages/frontend/src/widgets/WidgetSlideshow.vue +++ b/packages/frontend/src/widgets/WidgetSlideshow.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref, shallowRef } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import * as os from '@/os.js'; import { useInterval } from '@/scripts/use-interval.js'; diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue index 0ebffa105e..070466f476 100644 --- a/packages/frontend/src/widgets/WidgetTimeline.vue +++ b/packages/frontend/src/widgets/WidgetTimeline.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import * as os from '@/os.js'; import MkContainer from '@/components/MkContainer.vue'; @@ -136,7 +136,7 @@ const choose = async (ev) => { text: i18n.ts._timelines.global, icon: 'ph-globe-hemisphere-west ph-bold ph-lg', action: () => { setSrc('global'); }, - }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => { + }, antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => { menuOpened.value = false; }); }; diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue index ea7b9078f3..738cd70b03 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; diff --git a/packages/frontend/src/widgets/WidgetUnixClock.vue b/packages/frontend/src/widgets/WidgetUnixClock.vue index 33585cd721..35f29b5e21 100644 --- a/packages/frontend/src/widgets/WidgetUnixClock.vue +++ b/packages/frontend/src/widgets/WidgetUnixClock.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onUnmounted, ref, watch } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; const name = 'unixClock'; diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue index 159318c2ca..f5c1d2d3a3 100644 --- a/packages/frontend/src/widgets/WidgetUserList.vue +++ b/packages/frontend/src/widgets/WidgetUserList.vue @@ -24,7 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; +import { ref } from 'vue'; +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'; @@ -57,9 +58,9 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name, emit, ); -let list = $ref(); -let users = $ref([]); -let fetching = $ref(true); +const list = ref(); +const users = ref([]); +const fetching = ref(true); async function chooseList() { const lists = await os.api('users/lists/list'); @@ -79,19 +80,19 @@ async function chooseList() { const fetch = () => { if (widgetProps.listId == null) { - fetching = false; + fetching.value = false; return; } os.api('users/lists/show', { listId: widgetProps.listId, }).then(_list => { - list = _list; + list.value = _list; os.api('users/show', { - userIds: list.userIds, + userIds: list.value.userIds, }).then(_users => { - users = _users; - fetching = false; + users.value = _users; + fetching.value = false; }); }); }; diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts index ae3bd09c86..b783d783bc 100644 --- a/packages/frontend/src/widgets/index.ts +++ b/packages/frontend/src/widgets/index.ts @@ -34,6 +34,7 @@ export default function(app: App) { app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue'))); app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue'))); app.component('WidgetSearch', defineAsyncComponent(() => import('./WidgetSearch.vue'))); + app.component('WidgetBirthdayFollowings', defineAsyncComponent(() => import('./WidgetBirthdayFollowings.vue'))); } export const widgets = [ @@ -65,4 +66,5 @@ export const widgets = [ 'userList', 'clicker', 'search', + 'birthdayFollowings', ]; diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue index c656d75429..9196ae209f 100644 --- a/packages/frontend/src/widgets/server-metric/cpu-mem.vue +++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue @@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onBeforeUnmount } from 'vue'; +import { onMounted, onBeforeUnmount, ref } from 'vue'; import { v4 as uuid } from 'uuid'; const props = defineProps<{ @@ -83,23 +83,23 @@ const props = defineProps<{ meta: any }>(); -let viewBoxX: number = $ref(50); -let viewBoxY: number = $ref(30); -let stats: any[] = $ref([]); +const viewBoxX = ref<number>(50); +const viewBoxY = ref<number>(30); +const stats = ref<any[]>([]); const cpuGradientId = uuid(); const cpuMaskId = uuid(); const memGradientId = uuid(); const memMaskId = uuid(); -let cpuPolylinePoints: string = $ref(''); -let memPolylinePoints: string = $ref(''); -let cpuPolygonPoints: string = $ref(''); -let memPolygonPoints: string = $ref(''); -let cpuHeadX: any = $ref(null); -let cpuHeadY: any = $ref(null); -let memHeadX: any = $ref(null); -let memHeadY: any = $ref(null); -let cpuP: string = $ref(''); -let memP: string = $ref(''); +const cpuPolylinePoints = ref<string>(''); +const memPolylinePoints = ref<string>(''); +const cpuPolygonPoints = ref<string>(''); +const memPolygonPoints = ref<string>(''); +const cpuHeadX = ref<any>(null); +const cpuHeadY = ref<any>(null); +const memHeadX = ref<any>(null); +const memHeadY = ref<any>(null); +const cpuP = ref<string>(''); +const memP = ref<string>(''); onMounted(() => { props.connection.on('stats', onStats); @@ -115,24 +115,24 @@ onBeforeUnmount(() => { }); function onStats(connStats) { - stats.push(connStats); - if (stats.length > 50) stats.shift(); + stats.value.push(connStats); + if (stats.value.length > 50) stats.value.shift(); - let cpuPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - s.cpu) * viewBoxY]); - let memPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - (s.mem.active / props.meta.mem.total)) * viewBoxY]); - cpuPolylinePoints = cpuPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); - memPolylinePoints = memPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); + let cpuPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - s.cpu) * viewBoxY.value]); + let memPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - (s.mem.active / props.meta.mem.total)) * viewBoxY.value]); + cpuPolylinePoints.value = cpuPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); + memPolylinePoints.value = memPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); - cpuPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${cpuPolylinePoints} ${viewBoxX},${viewBoxY}`; - memPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${memPolylinePoints} ${viewBoxX},${viewBoxY}`; + cpuPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${cpuPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`; + memPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${memPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`; - cpuHeadX = cpuPolylinePointsStats.at(-1)![0]; - cpuHeadY = cpuPolylinePointsStats.at(-1)![1]; - memHeadX = memPolylinePointsStats.at(-1)![0]; - memHeadY = memPolylinePointsStats.at(-1)![1]; + cpuHeadX.value = cpuPolylinePointsStats.at(-1)![0]; + cpuHeadY.value = cpuPolylinePointsStats.at(-1)![1]; + memHeadX.value = memPolylinePointsStats.at(-1)![0]; + memHeadY.value = memPolylinePointsStats.at(-1)![1]; - cpuP = (connStats.cpu * 100).toFixed(0); - memP = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0); + cpuP.value = (connStats.cpu * 100).toFixed(0); + memP.value = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0); } function onStatsLog(statsLog) { diff --git a/packages/frontend/src/widgets/server-metric/cpu.vue b/packages/frontend/src/widgets/server-metric/cpu.vue index 65da16a632..cffbdb27ce 100644 --- a/packages/frontend/src/widgets/server-metric/cpu.vue +++ b/packages/frontend/src/widgets/server-metric/cpu.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onBeforeUnmount } from 'vue'; +import { onMounted, onBeforeUnmount, ref } from 'vue'; import XPie from './pie.vue'; const props = defineProps<{ @@ -23,10 +23,10 @@ const props = defineProps<{ meta: any }>(); -let usage: number = $ref(0); +const usage = ref<number>(0); function onStats(stats) { - usage = stats.cpu; + usage.value = stats.cpu; } onMounted(() => { diff --git a/packages/frontend/src/widgets/server-metric/disk.vue b/packages/frontend/src/widgets/server-metric/disk.vue index b9774da0cf..18f8560265 100644 --- a/packages/frontend/src/widgets/server-metric/disk.vue +++ b/packages/frontend/src/widgets/server-metric/disk.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { computed } from 'vue'; import XPie from './pie.vue'; import bytes from '@/filters/bytes.js'; @@ -24,10 +24,10 @@ const props = defineProps<{ meta: any; // TODO }>(); -const usage = $computed(() => props.meta.fs.used / props.meta.fs.total); -const total = $computed(() => props.meta.fs.total); -const used = $computed(() => props.meta.fs.used); -const available = $computed(() => props.meta.fs.total - props.meta.fs.used); +const usage = computed(() => props.meta.fs.used / props.meta.fs.total); +const total = computed(() => props.meta.fs.total); +const used = computed(() => props.meta.fs.used); +const available = computed(() => props.meta.fs.total - props.meta.fs.used); </script> <style lang="scss" scoped> diff --git a/packages/frontend/src/widgets/server-metric/mem.vue b/packages/frontend/src/widgets/server-metric/mem.vue index 5a57ef6e1e..118fd68fe8 100644 --- a/packages/frontend/src/widgets/server-metric/mem.vue +++ b/packages/frontend/src/widgets/server-metric/mem.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onBeforeUnmount } from 'vue'; +import { onMounted, onBeforeUnmount, ref } from 'vue'; import XPie from './pie.vue'; import bytes from '@/filters/bytes.js'; @@ -25,16 +25,16 @@ const props = defineProps<{ meta: any }>(); -let usage: number = $ref(0); -let total: number = $ref(0); -let used: number = $ref(0); -let free: number = $ref(0); +const usage = ref<number>(0); +const total = ref<number>(0); +const used = ref<number>(0); +const free = ref<number>(0); function onStats(stats) { - usage = stats.mem.active / props.meta.mem.total; - total = props.meta.mem.total; - used = stats.mem.active; - free = total - used; + usage.value = stats.mem.active / props.meta.mem.total; + total.value = props.meta.mem.total; + used.value = stats.mem.active; + free.value = total.value - used.value; } onMounted(() => { diff --git a/packages/frontend/src/widgets/server-metric/net.vue b/packages/frontend/src/widgets/server-metric/net.vue index 5593128660..e6a8bfc22a 100644 --- a/packages/frontend/src/widgets/server-metric/net.vue +++ b/packages/frontend/src/widgets/server-metric/net.vue @@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onBeforeUnmount } from 'vue'; +import { onMounted, onBeforeUnmount, ref } from 'vue'; import bytes from '@/filters/bytes.js'; const props = defineProps<{ @@ -57,19 +57,19 @@ const props = defineProps<{ meta: any }>(); -let viewBoxX: number = $ref(50); -let viewBoxY: number = $ref(30); -let stats: any[] = $ref([]); -let inPolylinePoints: string = $ref(''); -let outPolylinePoints: string = $ref(''); -let inPolygonPoints: string = $ref(''); -let outPolygonPoints: string = $ref(''); -let inHeadX: any = $ref(null); -let inHeadY: any = $ref(null); -let outHeadX: any = $ref(null); -let outHeadY: any = $ref(null); -let inRecent: number = $ref(0); -let outRecent: number = $ref(0); +const viewBoxX = ref<number>(50); +const viewBoxY = ref<number>(30); +const stats = ref<any[]>([]); +const inPolylinePoints = ref<string>(''); +const outPolylinePoints = ref<string>(''); +const inPolygonPoints = ref<string>(''); +const outPolygonPoints = ref<string>(''); +const inHeadX = ref<any>(null); +const inHeadY = ref<any>(null); +const outHeadX = ref<any>(null); +const outHeadY = ref<any>(null); +const inRecent = ref<number>(0); +const outRecent = ref<number>(0); onMounted(() => { props.connection.on('stats', onStats); @@ -85,27 +85,27 @@ onBeforeUnmount(() => { }); function onStats(connStats) { - stats.push(connStats); - if (stats.length > 50) stats.shift(); + stats.value.push(connStats); + if (stats.value.length > 50) stats.value.shift(); - const inPeak = Math.max(1024 * 64, Math.max(...stats.map(s => s.net.rx))); - const outPeak = Math.max(1024 * 64, Math.max(...stats.map(s => s.net.tx))); + const inPeak = Math.max(1024 * 64, Math.max(...stats.value.map(s => s.net.rx))); + const outPeak = Math.max(1024 * 64, Math.max(...stats.value.map(s => s.net.tx))); - let inPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - (s.net.rx / inPeak)) * viewBoxY]); - let outPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - (s.net.tx / outPeak)) * viewBoxY]); - inPolylinePoints = inPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); - outPolylinePoints = outPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); + let inPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - (s.net.rx / inPeak)) * viewBoxY.value]); + let outPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - (s.net.tx / outPeak)) * viewBoxY.value]); + inPolylinePoints.value = inPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); + outPolylinePoints.value = outPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); - inPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${inPolylinePoints} ${viewBoxX},${viewBoxY}`; - outPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${outPolylinePoints} ${viewBoxX},${viewBoxY}`; + inPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${inPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`; + outPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${outPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`; - inHeadX = inPolylinePointsStats.at(-1)![0]; - inHeadY = inPolylinePointsStats.at(-1)![1]; - outHeadX = outPolylinePointsStats.at(-1)![0]; - outHeadY = outPolylinePointsStats.at(-1)![1]; + inHeadX.value = inPolylinePointsStats.at(-1)![0]; + inHeadY.value = inPolylinePointsStats.at(-1)![1]; + outHeadX.value = outPolylinePointsStats.at(-1)![0]; + outHeadY.value = outPolylinePointsStats.at(-1)![1]; - inRecent = connStats.net.rx; - outRecent = connStats.net.tx; + inRecent.value = connStats.net.rx; + outRecent.value = connStats.net.tx; } function onStatsLog(statsLog) { diff --git a/packages/frontend/src/widgets/server-metric/pie.vue b/packages/frontend/src/widgets/server-metric/pie.vue index c8a1496101..fd18a6a4f2 100644 --- a/packages/frontend/src/widgets/server-metric/pie.vue +++ b/packages/frontend/src/widgets/server-metric/pie.vue @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { computed } from 'vue'; const props = defineProps<{ value: number; @@ -36,8 +36,8 @@ const props = defineProps<{ const r = 0.45; -const color = $computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`); -const strokeDashoffset = $computed(() => (1 - props.value) * (Math.PI * (r * 2))); +const color = computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`); +const strokeDashoffset = computed(() => (1 - props.value) * (Math.PI * (r * 2))); </script> <style lang="scss" module> diff --git a/packages/frontend/test/home.test.ts b/packages/frontend/test/home.test.ts index 6d38b7e526..094ea071b9 100644 --- a/packages/frontend/test/home.test.ts +++ b/packages/frontend/test/home.test.ts @@ -3,13 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { describe, test, assert, afterEach } from 'vitest'; -import { render, cleanup, type RenderResult } from '@testing-library/vue'; +import { afterEach, assert, describe, test } from 'vitest'; +import { cleanup, render, type RenderResult } from '@testing-library/vue'; import './init'; import type * as Misskey from 'misskey-js'; import { directives } from '@/directives/index.js'; import { components } from '@/components/index.js'; import XHome from '@/pages/user/home.vue'; +import 'intersection-observer'; describe('XHome', () => { const renderHome = (user: Partial<Misskey.entities.UserDetailed>): RenderResult => { diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts index 986fa99c17..dfc02378d5 100644 --- a/packages/frontend/test/init.ts +++ b/packages/frontend/test/init.ts @@ -21,7 +21,35 @@ vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING vi.mock('@/store.js', () => { return { defaultStore: { - state: {}, + state: { + + // なんかtestがうまいこと動かないのでここに書く + dataSaver: { + media: false, + avatar: false, + urlPreview: false, + code: false, + }, + + }, }, }; }); + +// Add mocks for Web Audio API +const AudioNodeMock = vi.fn(() => ({ + connect: vi.fn(() => ({ connect: vi.fn() })), + start: vi.fn(), +})); + +const GainNodeMock = vi.fn(() => ({ + gain: vi.fn(), +})); + +const AudioContextMock = vi.fn(() => ({ + createBufferSource: vi.fn(() => new AudioNodeMock()), + createGain: vi.fn(() => new GainNodeMock()), + decodeAudioData: vi.fn(), +})); + +vi.stubGlobal('AudioContext', AudioContextMock); diff --git a/packages/frontend/test/url-preview.test.ts b/packages/frontend/test/url-preview.test.ts index 811f07d9c7..f760de9274 100644 --- a/packages/frontend/test/url-preview.test.ts +++ b/packages/frontend/test/url-preview.test.ts @@ -150,7 +150,7 @@ describe('MkUrlPreview', () => { }); assert.exists(iframe, 'iframe should exist'); assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share'); - assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-scripts allow-same-origin'); + assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin'); }); test('Loading a post in iframe', async () => { @@ -159,6 +159,6 @@ describe('MkUrlPreview', () => { }); assert.exists(iframe, 'iframe should exist'); assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share'); - assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-scripts allow-same-origin'); + assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin'); }); }); diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index ad8fd773b0..5d451c878c 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -33,7 +33,6 @@ ], "types": [ "vite/client", - "reactivity-transform/macros-global" ], "lib": [ "esnext", diff --git a/packages/frontend/vite.config.local-dev.ts b/packages/frontend/vite.config.local-dev.ts new file mode 100644 index 0000000000..aea46f4231 --- /dev/null +++ b/packages/frontend/vite.config.local-dev.ts @@ -0,0 +1,61 @@ +import dns from 'dns'; +import { defineConfig } from 'vite'; +import locales from '../../locales'; +import { getConfig } from './vite.config.js'; + +dns.setDefaultResultOrder('ipv4first'); + +const defaultConfig = getConfig(); + +const devConfig = { + // 基本の設定は vite.config.js から引き継ぐ + ...defaultConfig, + root: 'src', + publicDir: '../assets', + base: './', + server: { + host: 'localhost', + port: 5173, + proxy: { + '/api': { + changeOrigin: true, + target: 'http://localhost:3000/', + }, + '/assets': 'http://localhost:3000/', + '/static-assets': 'http://localhost:3000/', + '/client-assets': 'http://localhost:3000/', + '/files': 'http://localhost:3000/', + '/twemoji': 'http://localhost:3000/', + '/fluent-emoji': 'http://localhost:3000/', + '/sw.js': 'http://localhost:3000/', + '/streaming': { + target: 'ws://localhost:3000/', + ws: true, + }, + '/favicon.ico': 'http://localhost:3000/', + '/identicon': { + target: 'http://localhost:3000/', + rewrite(path) { + return path.replace('@localhost:5173', ''); + }, + }, + '/url': 'http://localhost:3000', + '/proxy': 'http://localhost:3000', + }, + }, + build: { + ...defaultConfig.build, + rollupOptions: { + ...defaultConfig.build?.rollupOptions, + input: 'index.html', + }, + }, + + define: { + ...defaultConfig.define, + _LANGS_FULL_: JSON.stringify(Object.entries(locales)), + }, +}; + +export default defineConfig(({ command, mode }) => devConfig); + diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 996e302d99..8a1c56e6b1 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -2,8 +2,6 @@ import path from 'path'; import pluginReplace from '@rollup/plugin-replace'; import pluginVue from '@vitejs/plugin-vue'; import { type UserConfig, defineConfig } from 'vite'; -// @ts-expect-error https://github.com/sxzz/unplugin-vue-macros/issues/257#issuecomment-1410752890 -import ReactivityTransform from '@vue-macros/reactivity-transform/vite'; import locales from '../../locales'; import meta from '../../package.json'; @@ -50,10 +48,7 @@ export function getConfig(): UserConfig { }, plugins: [ - pluginVue({ - reactivityTransform: true, - }), - ReactivityTransform(), + pluginVue(), pluginUnwindCssModuleClassName(), pluginJson5(), ...process.env.NODE_ENV === 'production' @@ -147,10 +142,14 @@ export function getConfig(): UserConfig { test: { environment: 'happy-dom', deps: { - inline: [ - // XXX: misskey-dev/browser-image-resizer has no "type": "module" - 'browser-image-resizer', - ], + optimizer: { + web: { + include: [ + // XXX: misskey-dev/browser-image-resizer has no "type": "module" + 'browser-image-resizer', + ], + }, + }, }, }, }; diff --git a/packages/frontend/vue-shims.d.ts b/packages/frontend/vue-shims.d.ts new file mode 100644 index 0000000000..eba994772d --- /dev/null +++ b/packages/frontend/vue-shims.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable */ +declare module "*.vue" { + import { defineComponent } from "vue"; + const component: ReturnType<typeof defineComponent>; + export default component; +} diff --git a/packages/misskey-js/.swcrc b/packages/misskey-js/.swcrc index d9f047b6ac..0504a2d389 100644 --- a/packages/misskey-js/.swcrc +++ b/packages/misskey-js/.swcrc @@ -11,7 +11,7 @@ "decoratorMetadata": true }, "experimental": { - "keepImportAttributes": true + "keepImportAssertions": true }, "baseUrl": "src", "paths": { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index b4eedb56a8..ea4e0c4163 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -21,57 +21,343 @@ declare namespace acct { } export { acct } -// Warning: (ae-forgotten-export) The symbol "TODO_2" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "operations" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type Ad = TODO_2; +type AdminAbuseUserReportsRequest = operations['admin/abuse-user-reports']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminInstanceMetadata = DetailedInstanceMetadata & { - blockedHosts: string[]; - silencedHosts: string[]; - app192IconUrl: string | null; - app512IconUrl: string | null; - manifestJsonOverride: string; -}; +type AdminAbuseUserReportsResponse = operations['admin/abuse-user-reports']['responses']['200']['content']['application/json']; // @public (undocumented) -type Announcement = { - id: ID; - createdAt: DateString; - updatedAt: DateString | null; - text: string; - title: string; - imageUrl: string | null; - display: 'normal' | 'banner' | 'dialog'; - icon: 'info' | 'warning' | 'error' | 'success'; - needConfirmationToRead: boolean; - forYou: boolean; - isRead?: boolean; -}; +type AdminAccountsCreateRequest = operations['admin/accounts/create']['requestBody']['content']['application/json']; // @public (undocumented) -type Antenna = { - id: ID; - createdAt: DateString; - name: string; - keywords: string[][]; - excludeKeywords: string[][]; - src: 'home' | 'all' | 'users' | 'list' | 'group'; - userListId: ID | null; - userGroupId: ID | null; - users: string[]; - caseSensitive: boolean; - localOnly: boolean; - notify: boolean; - withReplies: boolean; - withFile: boolean; - hasUnreadNote: boolean; +type AdminAccountsCreateResponse = operations['admin/accounts/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminAccountsDeleteRequest = operations['admin/accounts/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAccountsFindByEmailRequest = operations['admin/accounts/find-by-email']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAdCreateRequest = operations['admin/ad/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAdDeleteRequest = operations['admin/ad/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAdListRequest = operations['admin/ad/list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAdUpdateRequest = operations['admin/ad/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAnnouncementsCreateRequest = operations['admin/announcements/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAnnouncementsCreateResponse = operations['admin/announcements/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminAnnouncementsDeleteRequest = operations['admin/announcements/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAnnouncementsListRequest = operations['admin/announcements/list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAnnouncementsListResponse = operations['admin/announcements/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminAnnouncementsUpdateRequest = operations['admin/announcements/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAvatarDecorationsCreateRequest = operations['admin/avatar-decorations/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAvatarDecorationsDeleteRequest = operations['admin/avatar-decorations/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAvatarDecorationsListRequest = operations['admin/avatar-decorations/list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAvatarDecorationsListResponse = operations['admin/avatar-decorations/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminAvatarDecorationsUpdateRequest = operations['admin/avatar-decorations/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminDeleteAccountRequest = operations['admin/delete-account']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminDeleteAccountResponse = operations['admin/delete-account']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminDeleteAllFilesOfAUserRequest = operations['admin/delete-all-files-of-a-user']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminDriveFilesRequest = operations['admin/drive/files']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminDriveFilesResponse = operations['admin/drive/files']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminDriveShowFileRequest = operations['admin/drive/show-file']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminDriveShowFileResponse = operations['admin/drive/show-file']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiAddAliasesBulkRequest = operations['admin/emoji/add-aliases-bulk']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiAddRequest = operations['admin/emoji/add']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiCopyRequest = operations['admin/emoji/copy']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiCopyResponse = operations['admin/emoji/copy']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiDeleteBulkRequest = operations['admin/emoji/delete-bulk']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiDeleteRequest = operations['admin/emoji/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiImportZipRequest = operations['admin/emoji/import-zip']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiListRemoteRequest = operations['admin/emoji/list-remote']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiListRemoteResponse = operations['admin/emoji/list-remote']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiListRequest = operations['admin/emoji/list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiListResponse = operations['admin/emoji/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiRemoveAliasesBulkRequest = operations['admin/emoji/remove-aliases-bulk']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiSetAliasesBulkRequest = operations['admin/emoji/set-aliases-bulk']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiSetCategoryBulkRequest = operations['admin/emoji/set-category-bulk']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiSetLicenseBulkRequest = operations['admin/emoji/set-license-bulk']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminEmojiUpdateRequest = operations['admin/emoji/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminFederationDeleteAllFilesRequest = operations['admin/federation/delete-all-files']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminFederationRefreshRemoteInstanceMetadataRequest = operations['admin/federation/refresh-remote-instance-metadata']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminFederationRemoveAllFollowingRequest = operations['admin/federation/remove-all-following']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminFederationUpdateInstanceRequest = operations['admin/federation/update-instance']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminGetTableStatsResponse = operations['admin/get-table-stats']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminGetUserIpsRequest = operations['admin/get-user-ips']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminInviteCreateRequest = operations['admin/invite/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminInviteCreateResponse = operations['admin/invite/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminInviteListRequest = operations['admin/invite/list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminInviteListResponse = operations['admin/invite/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminMetaResponse = operations['admin/meta']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminPromoCreateRequest = operations['admin/promo/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminQueueDeliverDelayedResponse = operations['admin/queue/deliver-delayed']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminQueueInboxDelayedResponse = operations['admin/queue/inbox-delayed']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminQueuePromoteRequest = operations['admin/queue/promote']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminQueueStatsResponse = operations['admin/queue/stats']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminRelaysAddRequest = operations['admin/relays/add']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminRelaysAddResponse = operations['admin/relays/add']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminRelaysListResponse = operations['admin/relays/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminRelaysRemoveRequest = operations['admin/relays/remove']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminResetPasswordRequest = operations['admin/reset-password']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminResetPasswordResponse = operations['admin/reset-password']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminResolveAbuseUserReportRequest = operations['admin/resolve-abuse-user-report']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminRolesAssignRequest = operations['admin/roles/assign']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminRolesCreateRequest = operations['admin/roles/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminRolesCreateResponse = operations['admin/roles/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminRolesDeleteRequest = operations['admin/roles/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminRolesListResponse = operations['admin/roles/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminRolesShowRequest = operations['admin/roles/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminRolesShowResponse = operations['admin/roles/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminRolesUnassignRequest = operations['admin/roles/unassign']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminRolesUpdateDefaultPoliciesRequest = operations['admin/roles/update-default-policies']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminRolesUpdateRequest = operations['admin/roles/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminRolesUsersRequest = operations['admin/roles/users']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminSendEmailRequest = operations['admin/send-email']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminServerInfoResponse = operations['admin/server-info']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminShowModerationLogsRequest = operations['admin/show-moderation-logs']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminShowModerationLogsResponse = operations['admin/show-moderation-logs']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminShowUserRequest = operations['admin/show-user']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminShowUserResponse = operations['admin/show-user']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminShowUsersRequest = operations['admin/show-users']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminShowUsersResponse = operations['admin/show-users']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminSuspendUserRequest = operations['admin/suspend-user']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminUnsetUserAvatarRequest = operations['admin/unset-user-avatar']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminUnsetUserBannerRequest = operations['admin/unset-user-banner']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminUnsuspendUserRequest = operations['admin/unsuspend-user']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminUpdateMetaRequest = operations['admin/update-meta']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminUpdateUserNoteRequest = operations['admin/update-user-note']['requestBody']['content']['application/json']; + +// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type Announcement = components['schemas']['Announcement']; + +// @public (undocumented) +type AnnouncementCreated = { + announcement: Announcement; }; +// @public (undocumented) +type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type Antenna = components['schemas']['Antenna']; + +// @public (undocumented) +type AntennasCreateRequest = operations['antennas/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AntennasCreateResponse = operations['antennas/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AntennasDeleteRequest = operations['antennas/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AntennasListResponse = operations['antennas/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AntennasNotesRequest = operations['antennas/notes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AntennasNotesResponse = operations['antennas/notes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AntennasShowRequest = operations['antennas/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AntennasShowResponse = operations['antennas/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AntennasUpdateRequest = operations['antennas/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AntennasUpdateResponse = operations['antennas/update']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ApGetRequest = operations['ap/get']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ApGetResponse = operations['ap/get']['responses']['200']['content']['application/json']; + declare namespace api { export { isAPIError, + SwitchCaseResponseType, APIError, FetchLike, APIClient @@ -92,16 +378,6 @@ class APIClient { fetch: FetchLike; // (undocumented) origin: string; - // Warning: (ae-forgotten-export) The symbol "IsCaseMatched" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "GetCaseResult" needs to be exported by the entry point index.d.ts - // - // (undocumented) - request<E extends keyof Endpoints, P extends Endpoints[E]['req']>(endpoint: E, params?: P, credential?: string | null | undefined): Promise<Endpoints[E]['res'] extends { - $switch: { - $cases: [any, any][]; - $default: any; - }; - } ? IsCaseMatched<E, P, 0> extends true ? GetCaseResult<E, P, 0> : IsCaseMatched<E, P, 1> extends true ? GetCaseResult<E, P, 1> : IsCaseMatched<E, P, 2> extends true ? GetCaseResult<E, P, 2> : IsCaseMatched<E, P, 3> extends true ? GetCaseResult<E, P, 3> : IsCaseMatched<E, P, 4> extends true ? GetCaseResult<E, P, 4> : IsCaseMatched<E, P, 5> extends true ? GetCaseResult<E, P, 5> : IsCaseMatched<E, P, 6> extends true ? GetCaseResult<E, P, 6> : IsCaseMatched<E, P, 7> extends true ? GetCaseResult<E, P, 7> : IsCaseMatched<E, P, 8> extends true ? GetCaseResult<E, P, 8> : IsCaseMatched<E, P, 9> extends true ? GetCaseResult<E, P, 9> : Endpoints[E]['res']['$switch']['$default'] : Endpoints[E]['res']>; } // @public (undocumented) @@ -114,41 +390,70 @@ type APIError = { }; // @public (undocumented) -type App = TODO_2; +type App = components['schemas']['App']; // @public (undocumented) -type AuthSession = { - id: ID; - app: App; - token: string; -}; +type AppCreateRequest = operations['app/create']['requestBody']['content']['application/json']; // @public (undocumented) -type Blocking = { - id: ID; - createdAt: DateString; - blockeeId: User['id']; - blockee: UserDetailed; -}; +type AppCreateResponse = operations['app/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type Channel = { - id: ID; - lastNotedAt: Date | null; - userId: User['id'] | null; - user: User | null; - name: string; - description: string | null; - bannerId: DriveFile['id'] | null; - banner: DriveFile | null; - pinnedNoteIds: string[]; - color: string; - isArchived: boolean; - notesCount: number; - usersCount: number; - isSensitive: boolean; - allowRenoteToExternal: boolean; -}; +type AppShowRequest = operations['app/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AppShowResponse = operations['app/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ApShowRequest = operations['ap/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ApShowResponse = operations['ap/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AuthAcceptRequest = operations['auth/accept']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AuthSessionGenerateRequest = operations['auth/session/generate']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AuthSessionGenerateResponse = operations['auth/session/generate']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AuthSessionShowRequest = operations['auth/session/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AuthSessionShowResponse = operations['auth/session/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AuthSessionUserkeyRequest = operations['auth/session/userkey']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AuthSessionUserkeyResponse = operations['auth/session/userkey']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type Blocking = components['schemas']['Blocking']; + +// @public (undocumented) +type BlockingCreateRequest = operations['blocking/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type BlockingCreateResponse = operations['blocking/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type BlockingDeleteRequest = operations['blocking/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type BlockingDeleteResponse = operations['blocking/delete']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type BlockingListRequest = operations['blocking/list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type BlockingListResponse = operations['blocking/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type Channel = components['schemas']['Channel']; // Warning: (ae-forgotten-export) The symbol "AnyOf" needs to be exported by the entry point index.d.ts // @@ -197,16 +502,11 @@ export type Channels = { readAllUnreadMentions: () => void; unreadSpecifiedNote: (payload: Note['id']) => void; readAllUnreadSpecifiedNotes: () => void; - readAllMessagingMessages: () => void; - messagingMessage: (payload: MessagingMessage) => void; - unreadMessagingMessage: (payload: MessagingMessage) => void; readAllAntennas: () => void; unreadAntenna: (payload: Antenna) => void; readAllAnnouncements: () => void; myTokenRegenerated: () => void; - reversiNoInvites: () => void; - reversiInvited: (payload: FIXME) => void; - signin: (payload: FIXME) => void; + signin: (payload: Signin) => void; registryUpdated: (payload: { scope?: string[]; key: string; @@ -214,58 +514,116 @@ export type Channels = { }) => void; driveFileCreated: (payload: DriveFile) => void; readAntenna: (payload: Antenna) => void; + receiveFollowRequest: (payload: User) => void; + announcementCreated: (payload: AnnouncementCreated) => void; }; receives: null; }; homeTimeline: { - params: null; + params: { + withRenotes?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; localTimeline: { - params: null; + params: { + withRenotes?: boolean; + withReplies?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; hybridTimeline: { - params: null; + params: { + withRenotes?: boolean; + withReplies?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; globalTimeline: { - params: null; + params: { + withRenotes?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; - messaging: { + userList: { params: { - otherparty?: User['id'] | null; - group?: UserGroup['id'] | null; + listId: string; + withFiles?: boolean; }; events: { - message: (payload: MessagingMessage) => void; - deleted: (payload: MessagingMessage['id']) => void; - read: (payload: MessagingMessage['id'][]) => void; - typers: (payload: User[]) => void; + note: (payload: Note) => void; }; - receives: { - read: { - id: MessagingMessage['id']; - }; + receives: null; + }; + hashtag: { + params: { + q?: string; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + roleTimeline: { + params: { + roleId: string; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + antenna: { + params: { + antennaId: string; + }; + events: { + note: (payload: Note) => void; }; + receives: null; + }; + channel: { + params: { + channelId: string; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + drive: { + params: null; + events: { + fileCreated: (payload: DriveFile) => void; + fileDeleted: (payload: DriveFile['id']) => void; + fileUpdated: (payload: DriveFile) => void; + folderCreated: (payload: DriveFolder) => void; + folderDeleted: (payload: DriveFolder['id']) => void; + folderUpdated: (payload: DriveFile) => void; + }; + receives: null; }; serverStats: { params: null; events: { - stats: (payload: FIXME) => void; + stats: (payload: ServerStats) => void; + statsLog: (payload: ServerStatsLog) => void; }; receives: { requestLog: { @@ -277,7 +635,8 @@ export type Channels = { queueStats: { params: null; events: { - stats: (payload: FIXME) => void; + stats: (payload: QueueStats) => void; + statsLog: (payload: QueueStatsLog) => void; }; receives: { requestLog: { @@ -286,1952 +645,363 @@ export type Channels = { }; }; }; + admin: { + params: null; + events: { + newAbuseUserReport: { + id: string; + targetUserId: string; + reporterId: string; + comment: string; + }; + }; + receives: null; + }; }; // @public (undocumented) -type Clip = TODO_2; +type ChannelsCreateRequest = operations['channels/create']['requestBody']['content']['application/json']; // @public (undocumented) -type CustomEmoji = { - id: string; - name: string; - url: string; - category: string; - aliases: string[]; -}; +type ChannelsCreateResponse = operations['channels/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChannelsFavoriteRequest = operations['channels/favorite']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChannelsFeaturedResponse = operations['channels/featured']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChannelsFollowedRequest = operations['channels/followed']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChannelsFollowedResponse = operations['channels/followed']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChannelsFollowRequest = operations['channels/follow']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChannelsMyFavoritesResponse = operations['channels/my-favorites']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChannelsOwnedRequest = operations['channels/owned']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChannelsOwnedResponse = operations['channels/owned']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChannelsSearchRequest = operations['channels/search']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChannelsSearchResponse = operations['channels/search']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChannelsShowRequest = operations['channels/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChannelsShowResponse = operations['channels/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChannelsTimelineRequest = operations['channels/timeline']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChannelsTimelineResponse = operations['channels/timeline']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChannelsUnfavoriteRequest = operations['channels/unfavorite']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChannelsUnfollowRequest = operations['channels/unfollow']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChannelsUpdateRequest = operations['channels/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChannelsUpdateResponse = operations['channels/update']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChartsActiveUsersRequest = operations['charts/active-users']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChartsActiveUsersResponse = operations['charts/active-users']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChartsApRequestRequest = operations['charts/ap-request']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChartsApRequestResponse = operations['charts/ap-request']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChartsDriveRequest = operations['charts/drive']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChartsDriveResponse = operations['charts/drive']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChartsFederationRequest = operations['charts/federation']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChartsFederationResponse = operations['charts/federation']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChartsInstanceRequest = operations['charts/instance']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChartsInstanceResponse = operations['charts/instance']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChartsNotesRequest = operations['charts/notes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChartsNotesResponse = operations['charts/notes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChartsUserDriveRequest = operations['charts/user/drive']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChartsUserDriveResponse = operations['charts/user/drive']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChartsUserFollowingRequest = operations['charts/user/following']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChartsUserFollowingResponse = operations['charts/user/following']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChartsUserNotesRequest = operations['charts/user/notes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChartsUserNotesResponse = operations['charts/user/notes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChartsUserPvRequest = operations['charts/user/pv']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChartsUserPvResponse = operations['charts/user/pv']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChartsUserReactionsRequest = operations['charts/user/reactions']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChartsUserReactionsResponse = operations['charts/user/reactions']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ChartsUsersRequest = operations['charts/users']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChartsUsersResponse = operations['charts/users']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type Clip = components['schemas']['Clip']; + +// @public (undocumented) +type ClipsAddNoteRequest = operations['clips/add-note']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ClipsCreateRequest = operations['clips/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ClipsCreateResponse = operations['clips/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ClipsDeleteRequest = operations['clips/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ClipsFavoriteRequest = operations['clips/favorite']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ClipsListResponse = operations['clips/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ClipsMyFavoritesResponse = operations['clips/my-favorites']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ClipsNotesRequest = operations['clips/notes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ClipsNotesResponse = operations['clips/notes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ClipsRemoveNoteRequest = operations['clips/remove-note']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ClipsShowRequest = operations['clips/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ClipsShowResponse = operations['clips/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ClipsUnfavoriteRequest = operations['clips/unfavorite']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ClipsUpdateRequest = operations['clips/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ClipsUpdateResponse = operations['clips/update']['responses']['200']['content']['application/json']; // @public (undocumented) type DateString = string; // @public (undocumented) -type DetailedInstanceMetadata = LiteInstanceMetadata & { - pinnedPages: string[]; - pinnedClipId: string | null; - cacheRemoteFiles: boolean; - cacheRemoteSensitiveFiles: boolean; - requireSetup: boolean; - proxyAccountName: string | null; - features: Record<string, any>; +type DriveFile = components['schemas']['DriveFile']; + +// @public (undocumented) +type DriveFilesAttachedNotesRequest = operations['drive/files/attached-notes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFilesAttachedNotesResponse = operations['drive/files/attached-notes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveFilesCheckExistenceRequest = operations['drive/files/check-existence']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFilesCheckExistenceResponse = operations['drive/files/check-existence']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveFilesCreateRequest = operations['drive/files/create']['requestBody']['content']['multipart/form-data']; + +// @public (undocumented) +type DriveFilesCreateResponse = operations['drive/files/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveFilesDeleteRequest = operations['drive/files/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFilesFindByHashRequest = operations['drive/files/find-by-hash']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFilesFindByHashResponse = operations['drive/files/find-by-hash']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveFilesFindRequest = operations['drive/files/find']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFilesFindResponse = operations['drive/files/find']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveFilesRequest = operations['drive/files']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFilesResponse = operations['drive/files']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveFilesShowRequest = operations['drive/files/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFilesShowResponse = operations['drive/files/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveFilesUpdateRequest = operations['drive/files/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFilesUpdateResponse = operations['drive/files/update']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveFilesUploadFromUrlRequest = operations['drive/files/upload-from-url']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFolder = components['schemas']['DriveFolder']; + +// @public (undocumented) +type DriveFoldersCreateRequest = operations['drive/folders/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFoldersCreateResponse = operations['drive/folders/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveFoldersDeleteRequest = operations['drive/folders/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFoldersFindRequest = operations['drive/folders/find']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFoldersFindResponse = operations['drive/folders/find']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveFoldersRequest = operations['drive/folders']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFoldersResponse = operations['drive/folders']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveFoldersShowRequest = operations['drive/folders/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFoldersShowResponse = operations['drive/folders/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveFoldersUpdateRequest = operations['drive/folders/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFoldersUpdateResponse = operations['drive/folders/update']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveResponse = operations['drive']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DriveStreamRequest = operations['drive/stream']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveStreamResponse = operations['drive/stream']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type EmailAddressAvailableRequest = operations['email-address/available']['requestBody']['content']['application/json']; + +// @public (undocumented) +type EmailAddressAvailableResponse = operations['email-address/available']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type EmojiAdded = { + emoji: EmojiDetailed; }; // @public (undocumented) -type DriveFile = { - id: ID; - createdAt: DateString; - isSensitive: boolean; - name: string; - thumbnailUrl: string; - url: string; - type: string; - size: number; - md5: string; - blurhash: string; - comment: string | null; - properties: Record<string, any>; +type EmojiDeleted = { + emojis: EmojiDetailed[]; }; // @public (undocumented) -type DriveFolder = TODO_2; +type EmojiDetailed = components['schemas']['EmojiDetailed']; // @public (undocumented) -export type Endpoints = { - 'admin/abuse-user-reports': { - req: TODO; - res: TODO; - }; - 'admin/delete-all-files-of-a-user': { - req: { - userId: User['id']; - }; - res: null; - }; - 'admin/delete-logs': { - req: NoParams; - res: null; - }; - 'admin/get-index-stats': { - req: TODO; - res: TODO; - }; - 'admin/get-table-stats': { - req: TODO; - res: TODO; - }; - 'admin/invite': { - req: TODO; - res: TODO; - }; - 'admin/logs': { - req: TODO; - res: TODO; - }; - 'admin/meta': { - req: NoParams; - res: AdminInstanceMetadata; - }; - 'admin/reset-password': { - req: TODO; - res: TODO; - }; - 'admin/resolve-abuse-user-report': { - req: TODO; - res: TODO; - }; - 'admin/resync-chart': { - req: TODO; - res: TODO; - }; - 'admin/send-email': { - req: TODO; - res: TODO; - }; - 'admin/server-info': { - req: TODO; - res: TODO; - }; - 'admin/show-moderation-logs': { - req: TODO; - res: TODO; - }; - 'admin/show-user': { - req: TODO; - res: TODO; - }; - 'admin/show-users': { - req: TODO; - res: TODO; - }; - 'admin/silence-user': { - req: TODO; - res: TODO; - }; - 'admin/suspend-user': { - req: TODO; - res: TODO; - }; - 'admin/unsilence-user': { - req: TODO; - res: TODO; - }; - 'admin/unsuspend-user': { - req: TODO; - res: TODO; - }; - 'admin/update-meta': { - req: TODO; - res: TODO; - }; - 'admin/vacuum': { - req: TODO; - res: TODO; - }; - 'admin/accounts/create': { - req: TODO; - res: TODO; - }; - 'admin/ad/create': { - req: TODO; - res: TODO; - }; - 'admin/ad/delete': { - req: { - id: Ad['id']; - }; - res: null; - }; - 'admin/ad/list': { - req: TODO; - res: TODO; - }; - 'admin/ad/update': { - req: TODO; - res: TODO; - }; - 'admin/announcements/create': { - req: TODO; - res: TODO; - }; - 'admin/announcements/delete': { - req: { - id: Announcement['id']; - }; - res: null; - }; - 'admin/announcements/list': { - req: TODO; - res: TODO; - }; - 'admin/announcements/update': { - req: TODO; - res: TODO; - }; - 'admin/drive/clean-remote-files': { - req: TODO; - res: TODO; - }; - 'admin/drive/cleanup': { - req: TODO; - res: TODO; - }; - 'admin/drive/files': { - req: TODO; - res: TODO; - }; - 'admin/drive/show-file': { - req: TODO; - res: TODO; - }; - 'admin/emoji/add': { - req: TODO; - res: TODO; - }; - 'admin/emoji/copy': { - req: TODO; - res: TODO; - }; - 'admin/emoji/list-remote': { - req: TODO; - res: TODO; - }; - 'admin/emoji/list': { - req: TODO; - res: TODO; - }; - 'admin/emoji/remove': { - req: TODO; - res: TODO; - }; - 'admin/emoji/update': { - req: TODO; - res: TODO; - }; - 'admin/federation/delete-all-files': { - req: { - host: string; - }; - res: null; - }; - 'admin/federation/refresh-remote-instance-metadata': { - req: TODO; - res: TODO; - }; - 'admin/federation/remove-all-following': { - req: TODO; - res: TODO; - }; - 'admin/federation/update-instance': { - req: TODO; - res: TODO; - }; - 'admin/invite/create': { - req: TODO; - res: TODO; - }; - 'admin/invite/list': { - req: TODO; - res: TODO; - }; - 'admin/moderators/add': { - req: TODO; - res: TODO; - }; - 'admin/moderators/remove': { - req: TODO; - res: TODO; - }; - 'admin/promo/create': { - req: TODO; - res: TODO; - }; - 'admin/queue/clear': { - req: TODO; - res: TODO; - }; - 'admin/queue/deliver-delayed': { - req: TODO; - res: TODO; - }; - 'admin/queue/inbox-delayed': { - req: TODO; - res: TODO; - }; - 'admin/queue/jobs': { - req: TODO; - res: TODO; - }; - 'admin/queue/stats': { - req: TODO; - res: TODO; - }; - 'admin/relays/add': { - req: TODO; - res: TODO; - }; - 'admin/relays/list': { - req: TODO; - res: TODO; - }; - 'admin/relays/remove': { - req: TODO; - res: TODO; - }; - 'announcements': { - req: { - limit?: number; - withUnreads?: boolean; - sinceId?: Announcement['id']; - untilId?: Announcement['id']; - }; - res: Announcement[]; - }; - 'antennas/create': { - req: TODO; - res: Antenna; - }; - 'antennas/delete': { - req: { - antennaId: Antenna['id']; - }; - res: null; - }; - 'antennas/list': { - req: NoParams; - res: Antenna[]; - }; - 'antennas/notes': { - req: { - antennaId: Antenna['id']; - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - }; - res: Note[]; - }; - 'antennas/show': { - req: { - antennaId: Antenna['id']; - }; - res: Antenna; - }; - 'antennas/update': { - req: TODO; - res: Antenna; - }; - 'ap/get': { - req: { - uri: string; - }; - res: Record<string, any>; - }; - 'ap/show': { - req: { - uri: string; - }; - res: { - type: 'Note'; - object: Note; - } | { - type: 'User'; - object: UserDetailed; - }; - }; - 'app/create': { - req: TODO; - res: App; - }; - 'app/show': { - req: { - appId: App['id']; - }; - res: App; - }; - 'auth/accept': { - req: { - token: string; - }; - res: null; - }; - 'auth/session/generate': { - req: { - appSecret: string; - }; - res: { - token: string; - url: string; - }; - }; - 'auth/session/show': { - req: { - token: string; - }; - res: AuthSession; - }; - 'auth/session/userkey': { - req: { - appSecret: string; - token: string; - }; - res: { - accessToken: string; - user: User; - }; - }; - 'blocking/create': { - req: { - userId: User['id']; - }; - res: UserDetailed; - }; - 'blocking/delete': { - req: { - userId: User['id']; - }; - res: UserDetailed; - }; - 'blocking/list': { - req: { - limit?: number; - sinceId?: Blocking['id']; - untilId?: Blocking['id']; - }; - res: Blocking[]; - }; - 'channels/create': { - req: TODO; - res: TODO; - }; - 'channels/featured': { - req: TODO; - res: TODO; - }; - 'channels/follow': { - req: TODO; - res: TODO; - }; - 'channels/followed': { - req: TODO; - res: TODO; - }; - 'channels/owned': { - req: TODO; - res: TODO; - }; - 'channels/pin-note': { - req: TODO; - res: TODO; - }; - 'channels/show': { - req: TODO; - res: TODO; - }; - 'channels/timeline': { - req: TODO; - res: TODO; - }; - 'channels/unfollow': { - req: TODO; - res: TODO; - }; - 'channels/update': { - req: TODO; - res: TODO; - }; - 'charts/active-users': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: { - local: { - users: number[]; - }; - remote: { - users: number[]; - }; - }; - }; - 'charts/drive': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: { - local: { - decCount: number[]; - decSize: number[]; - incCount: number[]; - incSize: number[]; - totalCount: number[]; - totalSize: number[]; - }; - remote: { - decCount: number[]; - decSize: number[]; - incCount: number[]; - incSize: number[]; - totalCount: number[]; - totalSize: number[]; - }; - }; - }; - 'charts/federation': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: { - instance: { - dec: number[]; - inc: number[]; - total: number[]; - }; - }; - }; - 'charts/hashtag': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: TODO; - }; - 'charts/instance': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - host: string; - }; - res: { - drive: { - decFiles: number[]; - decUsage: number[]; - incFiles: number[]; - incUsage: number[]; - totalFiles: number[]; - totalUsage: number[]; - }; - followers: { - dec: number[]; - inc: number[]; - total: number[]; - }; - following: { - dec: number[]; - inc: number[]; - total: number[]; - }; - notes: { - dec: number[]; - inc: number[]; - total: number[]; - diffs: { - normal: number[]; - renote: number[]; - reply: number[]; - }; - }; - requests: { - failed: number[]; - received: number[]; - succeeded: number[]; - }; - users: { - dec: number[]; - inc: number[]; - total: number[]; - }; - }; - }; - 'charts/network': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: TODO; - }; - 'charts/notes': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: { - local: { - dec: number[]; - inc: number[]; - total: number[]; - diffs: { - normal: number[]; - renote: number[]; - reply: number[]; - }; - }; - remote: { - dec: number[]; - inc: number[]; - total: number[]; - diffs: { - normal: number[]; - renote: number[]; - reply: number[]; - }; - }; - }; - }; - 'charts/user/drive': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - userId: User['id']; - }; - res: { - decCount: number[]; - decSize: number[]; - incCount: number[]; - incSize: number[]; - totalCount: number[]; - totalSize: number[]; - }; - }; - 'charts/user/following': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - userId: User['id']; - }; - res: TODO; - }; - 'charts/user/notes': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - userId: User['id']; - }; - res: { - dec: number[]; - inc: number[]; - total: number[]; - diffs: { - normal: number[]; - renote: number[]; - reply: number[]; - }; - }; - }; - 'charts/user/reactions': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - userId: User['id']; - }; - res: TODO; - }; - 'charts/users': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: { - local: { - dec: number[]; - inc: number[]; - total: number[]; - }; - remote: { - dec: number[]; - inc: number[]; - total: number[]; - }; - }; - }; - 'clips/add-note': { - req: TODO; - res: TODO; - }; - 'clips/create': { - req: TODO; - res: TODO; - }; - 'clips/delete': { - req: { - clipId: Clip['id']; - }; - res: null; - }; - 'clips/list': { - req: TODO; - res: TODO; - }; - 'clips/notes': { - req: TODO; - res: TODO; - }; - 'clips/show': { - req: TODO; - res: TODO; - }; - 'clips/update': { - req: TODO; - res: TODO; - }; - 'drive': { - req: NoParams; - res: { - capacity: number; - usage: number; - }; - }; - 'drive/files': { - req: { - folderId?: DriveFolder['id'] | null; - type?: DriveFile['type'] | null; - limit?: number; - sinceId?: DriveFile['id']; - untilId?: DriveFile['id']; - }; - res: DriveFile[]; - }; - 'drive/files/attached-notes': { - req: TODO; - res: TODO; - }; - 'drive/files/check-existence': { - req: TODO; - res: TODO; - }; - 'drive/files/create': { - req: { - folderId?: string; - name?: string; - comment?: string; - isSentisive?: boolean; - force?: boolean; - }; - res: DriveFile; - }; - 'drive/files/delete': { - req: { - fileId: DriveFile['id']; - }; - res: null; - }; - 'drive/files/find-by-hash': { - req: TODO; - res: TODO; - }; - 'drive/files/find': { - req: { - name: string; - folderId?: DriveFolder['id'] | null; - }; - res: DriveFile[]; - }; - 'drive/files/show': { - req: { - fileId?: DriveFile['id']; - url?: string; - }; - res: DriveFile; - }; - 'drive/files/update': { - req: { - fileId: DriveFile['id']; - folderId?: DriveFolder['id'] | null; - name?: string; - isSensitive?: boolean; - comment?: string | null; - }; - res: DriveFile; - }; - 'drive/files/upload-from-url': { - req: { - url: string; - folderId?: DriveFolder['id'] | null; - isSensitive?: boolean; - comment?: string | null; - marker?: string | null; - force?: boolean; - }; - res: null; - }; - 'drive/folders': { - req: { - folderId?: DriveFolder['id'] | null; - limit?: number; - sinceId?: DriveFile['id']; - untilId?: DriveFile['id']; - }; - res: DriveFolder[]; - }; - 'drive/folders/create': { - req: { - name?: string; - parentId?: DriveFolder['id'] | null; - }; - res: DriveFolder; - }; - 'drive/folders/delete': { - req: { - folderId: DriveFolder['id']; - }; - res: null; - }; - 'drive/folders/find': { - req: { - name: string; - parentId?: DriveFolder['id'] | null; - }; - res: DriveFolder[]; - }; - 'drive/folders/show': { - req: { - folderId: DriveFolder['id']; - }; - res: DriveFolder; - }; - 'drive/folders/update': { - req: { - folderId: DriveFolder['id']; - name?: string; - parentId?: DriveFolder['id'] | null; - }; - res: DriveFolder; - }; - 'drive/stream': { - req: { - type?: DriveFile['type'] | null; - limit?: number; - sinceId?: DriveFile['id']; - untilId?: DriveFile['id']; - }; - res: DriveFile[]; - }; - 'endpoint': { - req: { - endpoint: string; - }; - res: { - params: { - name: string; - type: string; - }[]; - }; - }; - 'endpoints': { - req: NoParams; - res: string[]; - }; - 'federation/dns': { - req: { - host: string; - }; - res: { - a: string[]; - aaaa: string[]; - cname: string[]; - txt: string[]; - }; - }; - 'federation/followers': { - req: { - host: string; - limit?: number; - sinceId?: Following['id']; - untilId?: Following['id']; - }; - res: FollowingFolloweePopulated[]; - }; - 'federation/following': { - req: { - host: string; - limit?: number; - sinceId?: Following['id']; - untilId?: Following['id']; - }; - res: FollowingFolloweePopulated[]; - }; - 'federation/instances': { - req: { - host?: string | null; - blocked?: boolean | null; - notResponding?: boolean | null; - suspended?: boolean | null; - federating?: boolean | null; - subscribing?: boolean | null; - publishing?: boolean | null; - limit?: number; - offset?: number; - sort?: '+pubSub' | '-pubSub' | '+notes' | '-notes' | '+users' | '-users' | '+following' | '-following' | '+followers' | '-followers' | '+caughtAt' | '-caughtAt' | '+lastCommunicatedAt' | '-lastCommunicatedAt' | '+driveUsage' | '-driveUsage' | '+driveFiles' | '-driveFiles'; - }; - res: Instance[]; - }; - 'federation/show-instance': { - req: { - host: string; - }; - res: Instance; - }; - 'federation/update-remote-user': { - req: { - userId: User['id']; - }; - res: null; - }; - 'federation/users': { - req: { - host: string; - limit?: number; - sinceId?: User['id']; - untilId?: User['id']; - }; - res: UserDetailed[]; - }; - 'following/create': { - req: { - userId: User['id']; - withReplies?: boolean; - }; - res: User; - }; - 'following/delete': { - req: { - userId: User['id']; - }; - res: User; - }; - 'following/requests/accept': { - req: { - userId: User['id']; - }; - res: null; - }; - 'following/requests/cancel': { - req: { - userId: User['id']; - }; - res: User; - }; - 'following/requests/list': { - req: NoParams; - res: FollowRequest[]; - }; - 'following/requests/reject': { - req: { - userId: User['id']; - }; - res: null; - }; - 'gallery/featured': { - req: null; - res: GalleryPost[]; - }; - 'gallery/popular': { - req: null; - res: GalleryPost[]; - }; - 'gallery/posts': { - req: { - limit?: number; - sinceId?: GalleryPost['id']; - untilId?: GalleryPost['id']; - }; - res: GalleryPost[]; - }; - 'gallery/posts/create': { - req: { - title: GalleryPost['title']; - description?: GalleryPost['description']; - fileIds: GalleryPost['fileIds']; - isSensitive?: GalleryPost['isSensitive']; - }; - res: GalleryPost; - }; - 'gallery/posts/delete': { - req: { - postId: GalleryPost['id']; - }; - res: null; - }; - 'gallery/posts/like': { - req: { - postId: GalleryPost['id']; - }; - res: null; - }; - 'gallery/posts/show': { - req: { - postId: GalleryPost['id']; - }; - res: GalleryPost; - }; - 'gallery/posts/unlike': { - req: { - postId: GalleryPost['id']; - }; - res: null; - }; - 'gallery/posts/update': { - req: { - postId: GalleryPost['id']; - title: GalleryPost['title']; - description?: GalleryPost['description']; - fileIds: GalleryPost['fileIds']; - isSensitive?: GalleryPost['isSensitive']; - }; - res: GalleryPost; - }; - 'games/reversi/games': { - req: TODO; - res: TODO; - }; - 'games/reversi/games/show': { - req: TODO; - res: TODO; - }; - 'games/reversi/games/surrender': { - req: TODO; - res: TODO; - }; - 'games/reversi/invitations': { - req: TODO; - res: TODO; - }; - 'games/reversi/match': { - req: TODO; - res: TODO; - }; - 'games/reversi/match/cancel': { - req: TODO; - res: TODO; - }; - 'get-online-users-count': { - req: NoParams; - res: { - count: number; - }; - }; - 'hashtags/list': { - req: TODO; - res: TODO; - }; - 'hashtags/search': { - req: TODO; - res: TODO; - }; - 'hashtags/show': { - req: TODO; - res: TODO; - }; - 'hashtags/trend': { - req: TODO; - res: TODO; - }; - 'hashtags/users': { - req: TODO; - res: TODO; - }; - 'i': { - req: NoParams; - res: User; - }; - 'i/apps': { - req: TODO; - res: TODO; - }; - 'i/authorized-apps': { - req: TODO; - res: TODO; - }; - 'i/change-password': { - req: TODO; - res: TODO; - }; - 'i/delete-account': { - req: { - password: string; - }; - res: null; - }; - 'i/export-blocking': { - req: TODO; - res: TODO; - }; - 'i/export-following': { - req: TODO; - res: TODO; - }; - 'i/export-mute': { - req: TODO; - res: TODO; - }; - 'i/export-notes': { - req: TODO; - res: TODO; - }; - 'i/export-user-lists': { - req: TODO; - res: TODO; - }; - 'i/favorites': { - req: { - limit?: number; - sinceId?: NoteFavorite['id']; - untilId?: NoteFavorite['id']; - }; - res: NoteFavorite[]; - }; - 'i/gallery/likes': { - req: TODO; - res: TODO; - }; - 'i/gallery/posts': { - req: TODO; - res: TODO; - }; - 'i/import-following': { - req: TODO; - res: TODO; - }; - 'i/import-user-lists': { - req: TODO; - res: TODO; - }; - 'i/move': { - req: TODO; - res: TODO; - }; - 'i/notifications': { - req: { - limit?: number; - sinceId?: Notification_2['id']; - untilId?: Notification_2['id']; - following?: boolean; - markAsRead?: boolean; - includeTypes?: Notification_2['type'][]; - excludeTypes?: Notification_2['type'][]; - }; - res: Notification_2[]; - }; - 'i/page-likes': { - req: TODO; - res: TODO; - }; - 'i/pages': { - req: TODO; - res: TODO; - }; - 'i/pin': { - req: { - noteId: Note['id']; - }; - res: MeDetailed; - }; - 'i/read-all-messaging-messages': { - req: TODO; - res: TODO; - }; - 'i/read-all-unread-notes': { - req: TODO; - res: TODO; - }; - 'i/read-announcement': { - req: TODO; - res: TODO; - }; - 'i/regenerate-token': { - req: { - password: string; - }; - res: null; - }; - 'i/registry/get-all': { - req: { - scope?: string[]; - }; - res: Record<string, any>; - }; - 'i/registry/get-detail': { - req: { - key: string; - scope?: string[]; - }; - res: { - updatedAt: DateString; - value: any; - }; - }; - 'i/registry/get': { - req: { - key: string; - scope?: string[]; - }; - res: any; - }; - 'i/registry/keys-with-type': { - req: { - scope?: string[]; - }; - res: Record<string, 'null' | 'array' | 'number' | 'string' | 'boolean' | 'object'>; - }; - 'i/registry/keys': { - req: { - scope?: string[]; - }; - res: string[]; - }; - 'i/registry/remove': { - req: { - key: string; - scope?: string[]; - }; - res: null; - }; - 'i/registry/set': { - req: { - key: string; - value: any; - scope?: string[]; - }; - res: null; - }; - 'i/revoke-token': { - req: TODO; - res: TODO; - }; - 'i/signin-history': { - req: { - limit?: number; - sinceId?: Signin['id']; - untilId?: Signin['id']; - }; - res: Signin[]; - }; - 'i/unpin': { - req: { - noteId: Note['id']; - }; - res: MeDetailed; - }; - 'i/update-email': { - req: { - password: string; - email?: string | null; - }; - res: MeDetailed; - }; - 'i/update': { - req: { - name?: string | null; - description?: string | null; - lang?: string | null; - location?: string | null; - birthday?: string | null; - avatarId?: DriveFile['id'] | null; - bannerId?: DriveFile['id'] | null; - fields?: { - name: string; - value: string; - }[]; - isLocked?: boolean; - isExplorable?: boolean; - hideOnlineStatus?: boolean; - carefulBot?: boolean; - autoAcceptFollowed?: boolean; - noCrawle?: boolean; - isBot?: boolean; - isCat?: boolean; - speakAsCat?: boolean; - injectFeaturedNote?: boolean; - receiveAnnouncementEmail?: boolean; - alwaysMarkNsfw?: boolean; - mutedWords?: string[][]; - notificationRecieveConfig?: any; - emailNotificationTypes?: string[]; - alsoKnownAs?: string[]; - }; - res: MeDetailed; - }; - 'i/user-group-invites': { - req: TODO; - res: TODO; - }; - 'i/2fa/done': { - req: TODO; - res: TODO; - }; - 'i/2fa/key-done': { - req: TODO; - res: TODO; - }; - 'i/2fa/password-less': { - req: TODO; - res: TODO; - }; - 'i/2fa/register-key': { - req: TODO; - res: TODO; - }; - 'i/2fa/register': { - req: TODO; - res: TODO; - }; - 'i/2fa/remove-key': { - req: TODO; - res: TODO; - }; - 'i/2fa/unregister': { - req: TODO; - res: TODO; - }; - 'invite/create': { - req: NoParams; - res: Invite; - }; - 'invite/delete': { - req: { - inviteId: Invite['id']; - }; - res: null; - }; - 'invite/list': { - req: { - limit?: number; - sinceId?: Invite['id']; - untilId?: Invite['id']; - }; - res: Invite[]; - }; - 'invite/limit': { - req: NoParams; - res: InviteLimit; - }; - 'messaging/history': { - req: { - limit?: number; - group?: boolean; - }; - res: MessagingMessage[]; - }; - 'messaging/messages': { - req: { - userId?: User['id']; - groupId?: UserGroup['id']; - limit?: number; - sinceId?: MessagingMessage['id']; - untilId?: MessagingMessage['id']; - markAsRead?: boolean; - }; - res: MessagingMessage[]; - }; - 'messaging/messages/create': { - req: { - userId?: User['id']; - groupId?: UserGroup['id']; - text?: string; - fileId?: DriveFile['id']; - }; - res: MessagingMessage; - }; - 'messaging/messages/delete': { - req: { - messageId: MessagingMessage['id']; - }; - res: null; - }; - 'messaging/messages/read': { - req: { - messageId: MessagingMessage['id']; - }; - res: null; - }; - 'meta': { - req: { - detail?: boolean; - }; - res: { - $switch: { - $cases: [ - [ - { - detail: true; - }, - DetailedInstanceMetadata - ], - [ - { - detail: false; - }, - LiteInstanceMetadata - ], - [ - { - detail: boolean; - }, - LiteInstanceMetadata | DetailedInstanceMetadata - ] - ]; - $default: LiteInstanceMetadata; - }; - }; - }; - 'miauth/gen-token': { - req: TODO; - res: TODO; - }; - 'mute/create': { - req: TODO; - res: TODO; - }; - 'mute/delete': { - req: { - userId: User['id']; - }; - res: null; - }; - 'mute/list': { - req: TODO; - res: TODO; - }; - 'my/apps': { - req: TODO; - res: TODO; - }; - 'notes': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - }; - res: Note[]; - }; - 'notes/children': { - req: { - noteId: Note['id']; - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - }; - res: Note[]; - }; - 'notes/clips': { - req: TODO; - res: TODO; - }; - 'notes/conversation': { - req: TODO; - res: TODO; - }; - 'notes/create': { - req: { - visibility?: 'public' | 'home' | 'followers' | 'specified'; - visibleUserIds?: User['id'][]; - text?: null | string; - cw?: null | string; - viaMobile?: boolean; - localOnly?: boolean; - fileIds?: DriveFile['id'][]; - replyId?: null | Note['id']; - renoteId?: null | Note['id']; - channelId?: null | Channel['id']; - poll?: null | { - choices: string[]; - multiple?: boolean; - expiresAt?: null | number; - expiredAfter?: null | number; - }; - }; - res: { - createdNote: Note; - }; - }; - 'notes/delete': { - req: { - noteId: Note['id']; - }; - res: null; - }; - 'notes/favorites/create': { - req: { - noteId: Note['id']; - }; - res: null; - }; - 'notes/favorites/delete': { - req: { - noteId: Note['id']; - }; - res: null; - }; - 'notes/featured': { - req: TODO; - res: Note[]; - }; - 'notes/global-timeline': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - sinceDate?: number; - untilDate?: number; - }; - res: Note[]; - }; - 'notes/hybrid-timeline': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - sinceDate?: number; - untilDate?: number; - }; - res: Note[]; - }; - 'notes/local-timeline': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - sinceDate?: number; - untilDate?: number; - }; - res: Note[]; - }; - 'notes/mentions': { - req: { - following?: boolean; - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - }; - res: Note[]; - }; - 'notes/polls/recommendation': { - req: TODO; - res: TODO; - }; - 'notes/polls/vote': { - req: { - noteId: Note['id']; - choice: number; - }; - res: null; - }; - 'notes/reactions': { - req: { - noteId: Note['id']; - type?: string | null; - limit?: number; - }; - res: NoteReaction[]; - }; - 'notes/reactions/create': { - req: { - noteId: Note['id']; - reaction: string; - }; - res: null; - }; - 'notes/reactions/delete': { - req: { - noteId: Note['id']; - }; - res: null; - }; - 'notes/renotes': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - noteId: Note['id']; - }; - res: Note[]; - }; - 'notes/replies': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - noteId: Note['id']; - }; - res: Note[]; - }; - 'notes/search-by-tag': { - req: TODO; - res: TODO; - }; - 'notes/search': { - req: TODO; - res: TODO; - }; - 'notes/show': { - req: { - noteId: Note['id']; - }; - res: Note; - }; - 'notes/state': { - req: TODO; - res: TODO; - }; - 'notes/timeline': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - sinceDate?: number; - untilDate?: number; - }; - res: Note[]; - }; - 'notes/unrenote': { - req: { - noteId: Note['id']; - }; - res: null; - }; - 'notes/user-list-timeline': { - req: { - listId: UserList['id']; - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - sinceDate?: number; - untilDate?: number; - }; - res: Note[]; - }; - 'notes/watching/create': { - req: TODO; - res: TODO; - }; - 'notes/watching/delete': { - req: { - noteId: Note['id']; - }; - res: null; - }; - 'notifications/create': { - req: { - body: string; - header?: string | null; - icon?: string | null; - }; - res: null; - }; - 'notifications/test-notification': { - req: NoParams; - res: null; - }; - 'notifications/mark-all-as-read': { - req: NoParams; - res: null; - }; - 'page-push': { - req: { - pageId: Page['id']; - event: string; - var?: any; - }; - res: null; - }; - 'pages/create': { - req: TODO; - res: Page; - }; - 'pages/delete': { - req: { - pageId: Page['id']; - }; - res: null; - }; - 'pages/featured': { - req: NoParams; - res: Page[]; - }; - 'pages/like': { - req: { - pageId: Page['id']; - }; - res: null; - }; - 'pages/show': { - req: { - pageId?: Page['id']; - name?: string; - username?: string; - }; - res: Page; - }; - 'pages/unlike': { - req: { - pageId: Page['id']; - }; - res: null; - }; - 'pages/update': { - req: TODO; - res: null; - }; - 'ping': { - req: NoParams; - res: { - pong: number; - }; - }; - 'pinned-users': { - req: TODO; - res: TODO; - }; - 'promo/read': { - req: TODO; - res: TODO; - }; - 'request-reset-password': { - req: { - username: string; - email: string; - }; - res: null; - }; - 'reset-password': { - req: { - token: string; - password: string; - }; - res: null; - }; - 'room/show': { - req: TODO; - res: TODO; - }; - 'room/update': { - req: TODO; - res: TODO; - }; - 'signup': { - req: { - username: string; - password: string; - host?: string; - invitationCode?: string; - emailAddress?: string; - 'hcaptcha-response'?: string; - 'g-recaptcha-response'?: string; - 'turnstile-response'?: string; - }; - res: MeSignup | null; - }; - 'stats': { - req: NoParams; - res: Stats; - }; - 'server-info': { - req: NoParams; - res: ServerInfo; - }; - 'sw/register': { - req: TODO; - res: TODO; - }; - 'username/available': { - req: { - username: string; - }; - res: { - available: boolean; - }; - }; - 'users': { - req: { - limit?: number; - offset?: number; - sort?: UserSorting; - origin?: OriginType; - }; - res: User[]; - }; - 'users/clips': { - req: TODO; - res: TODO; - }; - 'users/followers': { - req: { - userId?: User['id']; - username?: User['username']; - host?: User['host'] | null; - limit?: number; - sinceId?: Following['id']; - untilId?: Following['id']; - }; - res: FollowingFollowerPopulated[]; - }; - 'users/following': { - req: { - userId?: User['id']; - username?: User['username']; - host?: User['host'] | null; - limit?: number; - sinceId?: Following['id']; - untilId?: Following['id']; - }; - res: FollowingFolloweePopulated[]; - }; - 'users/gallery/posts': { - req: TODO; - res: TODO; - }; - 'users/get-frequently-replied-users': { - req: TODO; - res: TODO; - }; - 'users/groups/create': { - req: TODO; - res: TODO; - }; - 'users/groups/delete': { - req: { - groupId: UserGroup['id']; - }; - res: null; - }; - 'users/groups/invitations/accept': { - req: TODO; - res: TODO; - }; - 'users/groups/invitations/reject': { - req: TODO; - res: TODO; - }; - 'users/groups/invite': { - req: TODO; - res: TODO; - }; - 'users/groups/joined': { - req: TODO; - res: TODO; - }; - 'users/groups/owned': { - req: TODO; - res: TODO; - }; - 'users/groups/pull': { - req: TODO; - res: TODO; - }; - 'users/groups/show': { - req: TODO; - res: TODO; - }; - 'users/groups/transfer': { - req: TODO; - res: TODO; - }; - 'users/groups/update': { - req: TODO; - res: TODO; - }; - 'users/lists/create': { - req: { - name: string; - }; - res: UserList; - }; - 'users/lists/delete': { - req: { - listId: UserList['id']; - }; - res: null; - }; - 'users/lists/list': { - req: NoParams; - res: UserList[]; - }; - 'users/lists/pull': { - req: { - listId: UserList['id']; - userId: User['id']; - }; - res: null; - }; - 'users/lists/push': { - req: { - listId: UserList['id']; - userId: User['id']; - }; - res: null; - }; - 'users/lists/show': { - req: { - listId: UserList['id']; - }; - res: UserList; - }; - 'users/lists/update': { - req: { - listId: UserList['id']; - name: string; - }; - res: UserList; - }; - 'users/notes': { - req: { - userId: User['id']; - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - sinceDate?: number; - untilDate?: number; - }; - res: Note[]; - }; - 'users/pages': { - req: TODO; - res: TODO; - }; - 'users/flashs': { - req: TODO; - res: TODO; - }; - 'users/recommendation': { - req: TODO; - res: TODO; - }; - 'users/relation': { - req: TODO; - res: TODO; - }; - 'users/report-abuse': { - req: TODO; - res: TODO; - }; - 'users/search-by-username-and-host': { - req: TODO; - res: TODO; - }; - 'users/search': { - req: TODO; - res: TODO; - }; +type EmojiRequest = operations['emoji']['requestBody']['content']['application/json']; + +// @public (undocumented) +type EmojiResponse = operations['emoji']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type EmojiSimple = components['schemas']['EmojiSimple']; + +// @public (undocumented) +type EmojisResponse = operations['emojis']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type EmojiUpdated = { + emojis: EmojiDetailed[]; +}; + +// @public (undocumented) +type EmptyRequest = Record<string, unknown> | undefined; + +// @public (undocumented) +type EmptyResponse = Record<string, unknown> | undefined; + +// @public (undocumented) +type EndpointRequest = operations['endpoint']['requestBody']['content']['application/json']; + +// Warning: (ae-forgotten-export) The symbol "Overwrite" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Endpoints_2" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type Endpoints = Overwrite<Endpoints_2, { 'users/show': { - req: ShowUserReq | { - userIds: User['id'][]; - }; + req: UsersShowRequest; res: { $switch: { $cases: [ [ { - userIds: User['id'][]; + userIds?: string[]; }, UserDetailed[] ] @@ -2240,77 +1010,615 @@ export type Endpoints = { }; }; }; - 'fetch-rss': { - req: { - url: string; - }; - res: TODO; - }; - 'fetch-external-resources': { - req: { - url: string; - hash: string; - }; - res: { - type: string; - data: string; - }; - }; -}; +}>; + +// @public (undocumented) +type EndpointsResponse = operations['endpoints']['responses']['200']['content']['application/json']; declare namespace entities { export { ID, DateString, - User, + PageEvent, + ModerationLog, + ServerStats, + ServerStatsLog, + QueueStats, + QueueStatsLog, + EmojiAdded, + EmojiUpdated, + EmojiDeleted, + AnnouncementCreated, + EmptyRequest, + EmptyResponse, + AdminMetaResponse, + AdminAbuseUserReportsRequest, + AdminAbuseUserReportsResponse, + AdminAccountsCreateRequest, + AdminAccountsCreateResponse, + AdminAccountsDeleteRequest, + AdminAccountsFindByEmailRequest, + AdminAdCreateRequest, + AdminAdDeleteRequest, + AdminAdListRequest, + AdminAdUpdateRequest, + AdminAnnouncementsCreateRequest, + AdminAnnouncementsCreateResponse, + AdminAnnouncementsDeleteRequest, + AdminAnnouncementsListRequest, + AdminAnnouncementsListResponse, + AdminAnnouncementsUpdateRequest, + AdminAvatarDecorationsCreateRequest, + AdminAvatarDecorationsDeleteRequest, + AdminAvatarDecorationsListRequest, + AdminAvatarDecorationsListResponse, + AdminAvatarDecorationsUpdateRequest, + AdminDeleteAllFilesOfAUserRequest, + AdminUnsetUserAvatarRequest, + AdminUnsetUserBannerRequest, + AdminDriveFilesRequest, + AdminDriveFilesResponse, + AdminDriveShowFileRequest, + AdminDriveShowFileResponse, + AdminEmojiAddAliasesBulkRequest, + AdminEmojiAddRequest, + AdminEmojiCopyRequest, + AdminEmojiCopyResponse, + AdminEmojiDeleteBulkRequest, + AdminEmojiDeleteRequest, + AdminEmojiImportZipRequest, + AdminEmojiListRemoteRequest, + AdminEmojiListRemoteResponse, + AdminEmojiListRequest, + AdminEmojiListResponse, + AdminEmojiRemoveAliasesBulkRequest, + AdminEmojiSetAliasesBulkRequest, + AdminEmojiSetCategoryBulkRequest, + AdminEmojiSetLicenseBulkRequest, + AdminEmojiUpdateRequest, + AdminFederationDeleteAllFilesRequest, + AdminFederationRefreshRemoteInstanceMetadataRequest, + AdminFederationRemoveAllFollowingRequest, + AdminFederationUpdateInstanceRequest, + AdminGetTableStatsResponse, + AdminGetUserIpsRequest, + AdminInviteCreateRequest, + AdminInviteCreateResponse, + AdminInviteListRequest, + AdminInviteListResponse, + AdminPromoCreateRequest, + AdminQueueDeliverDelayedResponse, + AdminQueueInboxDelayedResponse, + AdminQueuePromoteRequest, + AdminQueueStatsResponse, + AdminRelaysAddRequest, + AdminRelaysAddResponse, + AdminRelaysListResponse, + AdminRelaysRemoveRequest, + AdminResetPasswordRequest, + AdminResetPasswordResponse, + AdminResolveAbuseUserReportRequest, + AdminSendEmailRequest, + AdminServerInfoResponse, + AdminShowModerationLogsRequest, + AdminShowModerationLogsResponse, + AdminShowUserRequest, + AdminShowUserResponse, + AdminShowUsersRequest, + AdminShowUsersResponse, + AdminSuspendUserRequest, + AdminUnsuspendUserRequest, + AdminUpdateMetaRequest, + AdminDeleteAccountRequest, + AdminDeleteAccountResponse, + AdminUpdateUserNoteRequest, + AdminRolesCreateRequest, + AdminRolesCreateResponse, + AdminRolesDeleteRequest, + AdminRolesListResponse, + AdminRolesShowRequest, + AdminRolesShowResponse, + AdminRolesUpdateRequest, + AdminRolesAssignRequest, + AdminRolesUnassignRequest, + AdminRolesUpdateDefaultPoliciesRequest, + AdminRolesUsersRequest, + AnnouncementsRequest, + AnnouncementsResponse, + AntennasCreateRequest, + AntennasCreateResponse, + AntennasDeleteRequest, + AntennasListResponse, + AntennasNotesRequest, + AntennasNotesResponse, + AntennasShowRequest, + AntennasShowResponse, + AntennasUpdateRequest, + AntennasUpdateResponse, + ApGetRequest, + ApGetResponse, + ApShowRequest, + ApShowResponse, + AppCreateRequest, + AppCreateResponse, + AppShowRequest, + AppShowResponse, + AuthAcceptRequest, + AuthSessionGenerateRequest, + AuthSessionGenerateResponse, + AuthSessionShowRequest, + AuthSessionShowResponse, + AuthSessionUserkeyRequest, + AuthSessionUserkeyResponse, + BlockingCreateRequest, + BlockingCreateResponse, + BlockingDeleteRequest, + BlockingDeleteResponse, + BlockingListRequest, + BlockingListResponse, + ChannelsCreateRequest, + ChannelsCreateResponse, + ChannelsFeaturedResponse, + ChannelsFollowRequest, + ChannelsFollowedRequest, + ChannelsFollowedResponse, + ChannelsOwnedRequest, + ChannelsOwnedResponse, + ChannelsShowRequest, + ChannelsShowResponse, + ChannelsTimelineRequest, + ChannelsTimelineResponse, + ChannelsUnfollowRequest, + ChannelsUpdateRequest, + ChannelsUpdateResponse, + ChannelsFavoriteRequest, + ChannelsUnfavoriteRequest, + ChannelsMyFavoritesResponse, + ChannelsSearchRequest, + ChannelsSearchResponse, + ChartsActiveUsersRequest, + ChartsActiveUsersResponse, + ChartsApRequestRequest, + ChartsApRequestResponse, + ChartsDriveRequest, + ChartsDriveResponse, + ChartsFederationRequest, + ChartsFederationResponse, + ChartsInstanceRequest, + ChartsInstanceResponse, + ChartsNotesRequest, + ChartsNotesResponse, + ChartsUserDriveRequest, + ChartsUserDriveResponse, + ChartsUserFollowingRequest, + ChartsUserFollowingResponse, + ChartsUserNotesRequest, + ChartsUserNotesResponse, + ChartsUserPvRequest, + ChartsUserPvResponse, + ChartsUserReactionsRequest, + ChartsUserReactionsResponse, + ChartsUsersRequest, + ChartsUsersResponse, + ClipsAddNoteRequest, + ClipsRemoveNoteRequest, + ClipsCreateRequest, + ClipsCreateResponse, + ClipsDeleteRequest, + ClipsListResponse, + ClipsNotesRequest, + ClipsNotesResponse, + ClipsShowRequest, + ClipsShowResponse, + ClipsUpdateRequest, + ClipsUpdateResponse, + ClipsFavoriteRequest, + ClipsUnfavoriteRequest, + ClipsMyFavoritesResponse, + DriveResponse, + DriveFilesRequest, + DriveFilesResponse, + DriveFilesAttachedNotesRequest, + DriveFilesAttachedNotesResponse, + DriveFilesCheckExistenceRequest, + DriveFilesCheckExistenceResponse, + DriveFilesCreateRequest, + DriveFilesCreateResponse, + DriveFilesDeleteRequest, + DriveFilesFindByHashRequest, + DriveFilesFindByHashResponse, + DriveFilesFindRequest, + DriveFilesFindResponse, + DriveFilesShowRequest, + DriveFilesShowResponse, + DriveFilesUpdateRequest, + DriveFilesUpdateResponse, + DriveFilesUploadFromUrlRequest, + DriveFoldersRequest, + DriveFoldersResponse, + DriveFoldersCreateRequest, + DriveFoldersCreateResponse, + DriveFoldersDeleteRequest, + DriveFoldersFindRequest, + DriveFoldersFindResponse, + DriveFoldersShowRequest, + DriveFoldersShowResponse, + DriveFoldersUpdateRequest, + DriveFoldersUpdateResponse, + DriveStreamRequest, + DriveStreamResponse, + EmailAddressAvailableRequest, + EmailAddressAvailableResponse, + EndpointRequest, + EndpointsResponse, + FederationFollowersRequest, + FederationFollowersResponse, + FederationFollowingRequest, + FederationFollowingResponse, + FederationInstancesRequest, + FederationInstancesResponse, + FederationShowInstanceRequest, + FederationShowInstanceResponse, + FederationUpdateRemoteUserRequest, + FederationUsersRequest, + FederationUsersResponse, + FederationStatsRequest, + FollowingCreateRequest, + FollowingCreateResponse, + FollowingDeleteRequest, + FollowingDeleteResponse, + FollowingUpdateRequest, + FollowingUpdateResponse, + FollowingUpdateAllRequest, + FollowingInvalidateRequest, + FollowingInvalidateResponse, + FollowingRequestsAcceptRequest, + FollowingRequestsCancelRequest, + FollowingRequestsCancelResponse, + FollowingRequestsListRequest, + FollowingRequestsListResponse, + FollowingRequestsRejectRequest, + GalleryFeaturedRequest, + GalleryFeaturedResponse, + GalleryPopularResponse, + GalleryPostsRequest, + GalleryPostsResponse, + GalleryPostsCreateRequest, + GalleryPostsCreateResponse, + GalleryPostsDeleteRequest, + GalleryPostsLikeRequest, + GalleryPostsShowRequest, + GalleryPostsShowResponse, + GalleryPostsUnlikeRequest, + GalleryPostsUpdateRequest, + GalleryPostsUpdateResponse, + GetAvatarDecorationsResponse, + HashtagsListRequest, + HashtagsListResponse, + HashtagsSearchRequest, + HashtagsSearchResponse, + HashtagsShowRequest, + HashtagsShowResponse, + HashtagsTrendResponse, + HashtagsUsersRequest, + HashtagsUsersResponse, + IResponse, + I2faDoneRequest, + I2faKeyDoneRequest, + I2faPasswordLessRequest, + I2faRegisterKeyRequest, + I2faRegisterRequest, + I2faUpdateKeyRequest, + I2faRemoveKeyRequest, + I2faUnregisterRequest, + IAppsRequest, + IAuthorizedAppsRequest, + IClaimAchievementRequest, + IChangePasswordRequest, + IDeleteAccountRequest, + IExportFollowingRequest, + IFavoritesRequest, + IFavoritesResponse, + IGalleryLikesRequest, + IGalleryLikesResponse, + IGalleryPostsRequest, + IGalleryPostsResponse, + IImportBlockingRequest, + IImportFollowingRequest, + IImportMutingRequest, + IImportUserListsRequest, + IImportAntennasRequest, + INotificationsRequest, + INotificationsResponse, + INotificationsGroupedRequest, + INotificationsGroupedResponse, + IPageLikesRequest, + IPageLikesResponse, + IPagesRequest, + IPagesResponse, + IPinRequest, + IPinResponse, + IReadAnnouncementRequest, + IRegenerateTokenRequest, + IRegistryGetAllRequest, + IRegistryGetDetailRequest, + IRegistryGetRequest, + IRegistryKeysWithTypeRequest, + IRegistryKeysRequest, + IRegistryRemoveRequest, + IRegistrySetRequest, + IRevokeTokenRequest, + ISigninHistoryRequest, + ISigninHistoryResponse, + IUnpinRequest, + IUnpinResponse, + IUpdateEmailRequest, + IUpdateRequest, + IUpdateResponse, + IMoveRequest, + IWebhooksCreateRequest, + IWebhooksShowRequest, + IWebhooksUpdateRequest, + IWebhooksDeleteRequest, + InviteCreateResponse, + InviteDeleteRequest, + InviteListRequest, + InviteListResponse, + InviteLimitResponse, + MetaRequest, + MetaResponse, + EmojisResponse, + EmojiRequest, + EmojiResponse, + MiauthGenTokenRequest, + MiauthGenTokenResponse, + MuteCreateRequest, + MuteDeleteRequest, + MuteListRequest, + MuteListResponse, + RenoteMuteCreateRequest, + RenoteMuteDeleteRequest, + RenoteMuteListRequest, + RenoteMuteListResponse, + MyAppsRequest, + MyAppsResponse, + NotesRequest, + NotesResponse, + NotesChildrenRequest, + NotesChildrenResponse, + NotesClipsRequest, + NotesClipsResponse, + NotesConversationRequest, + NotesConversationResponse, + NotesCreateRequest, + NotesCreateResponse, + NotesDeleteRequest, + NotesFavoritesCreateRequest, + NotesFavoritesDeleteRequest, + NotesFeaturedRequest, + NotesFeaturedResponse, + NotesGlobalTimelineRequest, + NotesGlobalTimelineResponse, + NotesHybridTimelineRequest, + NotesHybridTimelineResponse, + NotesLocalTimelineRequest, + NotesLocalTimelineResponse, + NotesMentionsRequest, + NotesMentionsResponse, + NotesPollsRecommendationRequest, + NotesPollsRecommendationResponse, + NotesPollsVoteRequest, + NotesReactionsRequest, + NotesReactionsResponse, + NotesReactionsCreateRequest, + NotesReactionsDeleteRequest, + NotesRenotesRequest, + NotesRenotesResponse, + NotesRepliesRequest, + NotesRepliesResponse, + NotesSearchByTagRequest, + NotesSearchByTagResponse, + NotesSearchRequest, + NotesSearchResponse, + NotesShowRequest, + NotesShowResponse, + NotesStateRequest, + NotesStateResponse, + NotesThreadMutingCreateRequest, + NotesThreadMutingDeleteRequest, + NotesTimelineRequest, + NotesTimelineResponse, + NotesTranslateRequest, + NotesTranslateResponse, + NotesUnrenoteRequest, + NotesUserListTimelineRequest, + NotesUserListTimelineResponse, + NotificationsCreateRequest, + PagePushRequest, + PagesCreateRequest, + PagesCreateResponse, + PagesDeleteRequest, + PagesFeaturedResponse, + PagesLikeRequest, + PagesShowRequest, + PagesShowResponse, + PagesUnlikeRequest, + PagesUpdateRequest, + FlashCreateRequest, + FlashDeleteRequest, + FlashFeaturedResponse, + FlashLikeRequest, + FlashShowRequest, + FlashShowResponse, + FlashUnlikeRequest, + FlashUpdateRequest, + FlashMyRequest, + FlashMyResponse, + FlashMyLikesRequest, + FlashMyLikesResponse, + PingResponse, + PinnedUsersResponse, + PromoReadRequest, + RolesListResponse, + RolesShowRequest, + RolesShowResponse, + RolesUsersRequest, + RolesNotesRequest, + RolesNotesResponse, + RequestResetPasswordRequest, + ResetPasswordRequest, + StatsResponse, + SwShowRegistrationRequest, + SwShowRegistrationResponse, + SwUpdateRegistrationRequest, + SwUpdateRegistrationResponse, + SwRegisterRequest, + SwRegisterResponse, + SwUnregisterRequest, + TestRequest, + UsernameAvailableRequest, + UsernameAvailableResponse, + UsersRequest, + UsersResponse, + UsersClipsRequest, + UsersClipsResponse, + UsersFollowersRequest, + UsersFollowersResponse, + UsersFollowingRequest, + UsersFollowingResponse, + UsersGalleryPostsRequest, + UsersGalleryPostsResponse, + UsersGetFrequentlyRepliedUsersRequest, + UsersGetFrequentlyRepliedUsersResponse, + UsersFeaturedNotesRequest, + UsersFeaturedNotesResponse, + UsersListsCreateRequest, + UsersListsCreateResponse, + UsersListsDeleteRequest, + UsersListsListRequest, + UsersListsListResponse, + UsersListsPullRequest, + UsersListsPushRequest, + UsersListsShowRequest, + UsersListsShowResponse, + UsersListsFavoriteRequest, + UsersListsUnfavoriteRequest, + UsersListsUpdateRequest, + UsersListsUpdateResponse, + UsersListsCreateFromPublicRequest, + UsersListsCreateFromPublicResponse, + UsersListsUpdateMembershipRequest, + UsersListsGetMembershipsRequest, + UsersNotesRequest, + UsersNotesResponse, + UsersPagesRequest, + UsersPagesResponse, + UsersFlashsRequest, + UsersFlashsResponse, + UsersReactionsRequest, + UsersReactionsResponse, + UsersRecommendationRequest, + UsersRecommendationResponse, + UsersRelationRequest, + UsersRelationResponse, + UsersReportAbuseRequest, + UsersSearchByUsernameAndHostRequest, + UsersSearchByUsernameAndHostResponse, + UsersSearchRequest, + UsersSearchResponse, + UsersShowRequest, + UsersShowResponse, + UsersAchievementsRequest, + UsersUpdateMemoRequest, + FetchRssRequest, + FetchExternalResourcesRequest, + RetentionResponse, + Error_2 as Error, UserLite, + UserDetailedNotMeOnly, + MeDetailedOnly, + UserDetailedNotMe, + MeDetailed, UserDetailed, - UserGroup, + User, UserList, - MeDetailed, - MeDetailedWithSecret, - MeSignup, - DriveFile, - DriveFolder, - GalleryPost, + Announcement, + App, Note, NoteReaction, + NoteFavorite, Notification_2 as Notification, - MessagingMessage, - CustomEmoji, - LiteInstanceMetadata, - DetailedInstanceMetadata, - InstanceMetadata, - AdminInstanceMetadata, - ServerInfo, - Stats, + DriveFile, + DriveFolder, + Following, + Muting, + RenoteMuting, + Blocking, + Hashtag, + InviteCode, Page, - PageEvent, - Announcement, + Channel, + QueueCount, Antenna, - App, - AuthSession, - Ad, Clip, - NoteFavorite, - FollowRequest, - Channel, - Following, - FollowingFolloweePopulated, - FollowingFollowerPopulated, - Blocking, - Instance, + FederationInstance, + GalleryPost, + EmojiSimple, + EmojiDetailed, + Flash, Signin, - Invite, - InviteLimit, - UserSorting, - OriginType, - ModerationLog + RoleLite, + Role } } export { entities } // @public (undocumented) +type Error_2 = components['schemas']['Error']; + +// @public (undocumented) +type FederationFollowersRequest = operations['federation/followers']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FederationFollowersResponse = operations['federation/followers']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FederationFollowingRequest = operations['federation/following']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FederationFollowingResponse = operations['federation/following']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FederationInstance = components['schemas']['FederationInstance']; + +// @public (undocumented) +type FederationInstancesRequest = operations['federation/instances']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FederationInstancesResponse = operations['federation/instances']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FederationShowInstanceRequest = operations['federation/show-instance']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FederationShowInstanceResponse = operations['federation/show-instance']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FederationStatsRequest = operations['federation/stats']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FederationUpdateRemoteUserRequest = operations['federation/update-remote-user']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FederationUsersRequest = operations['federation/users']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FederationUsersResponse = operations['federation/users']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FetchExternalResourcesRequest = operations['fetch-external-resources']['requestBody']['content']['application/json']; + +// @public (undocumented) type FetchLike = (input: string, init?: { method?: string; body?: string; @@ -2325,243 +1633,394 @@ type FetchLike = (input: string, init?: { }>; // @public (undocumented) -export const ffVisibility: readonly ["public", "followers", "private"]; +type FetchRssRequest = operations['fetch-rss']['requestBody']['content']['application/json']; // @public (undocumented) -type Following = { - id: ID; - createdAt: DateString; - followerId: User['id']; - followeeId: User['id']; -}; +type Flash = components['schemas']['Flash']; // @public (undocumented) -type FollowingFolloweePopulated = Following & { - followee: UserDetailed; -}; +type FlashCreateRequest = operations['flash/create']['requestBody']['content']['application/json']; // @public (undocumented) -type FollowingFollowerPopulated = Following & { - follower: UserDetailed; -}; +type FlashDeleteRequest = operations['flash/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type FollowRequest = { - id: ID; - follower: User; - followee: User; -}; +type FlashFeaturedResponse = operations['flash/featured']['responses']['200']['content']['application/json']; // @public (undocumented) -type GalleryPost = { - id: ID; - createdAt: DateString; - updatedAt: DateString; - userId: User['id']; - user: User; - title: string; - description: string | null; - fileIds: DriveFile['id'][]; - files: DriveFile[]; - isSensitive: boolean; - likedCount: number; - isLiked?: boolean; -}; +type FlashLikeRequest = operations['flash/like']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FlashMyLikesRequest = operations['flash/my-likes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FlashMyLikesResponse = operations['flash/my-likes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FlashMyRequest = operations['flash/my']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FlashMyResponse = operations['flash/my']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FlashShowRequest = operations['flash/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FlashShowResponse = operations['flash/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FlashUnlikeRequest = operations['flash/unlike']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FlashUpdateRequest = operations['flash/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +export const followersVisibilities: readonly ["public", "followers", "private"]; + +// @public (undocumented) +type Following = components['schemas']['Following']; + +// @public (undocumented) +type FollowingCreateRequest = operations['following/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FollowingCreateResponse = operations['following/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FollowingDeleteRequest = operations['following/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FollowingDeleteResponse = operations['following/delete']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FollowingInvalidateRequest = operations['following/invalidate']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FollowingInvalidateResponse = operations['following/invalidate']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FollowingRequestsAcceptRequest = operations['following/requests/accept']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FollowingRequestsCancelRequest = operations['following/requests/cancel']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FollowingRequestsCancelResponse = operations['following/requests/cancel']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FollowingRequestsListRequest = operations['following/requests/list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FollowingRequestsListResponse = operations['following/requests/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type FollowingRequestsRejectRequest = operations['following/requests/reject']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FollowingUpdateAllRequest = operations['following/update-all']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FollowingUpdateRequest = operations['following/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FollowingUpdateResponse = operations['following/update']['responses']['200']['content']['application/json']; + +// @public (undocumented) +export const followingVisibilities: readonly ["public", "followers", "private"]; + +// @public (undocumented) +type GalleryFeaturedRequest = operations['gallery/featured']['requestBody']['content']['application/json']; + +// @public (undocumented) +type GalleryFeaturedResponse = operations['gallery/featured']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type GalleryPopularResponse = operations['gallery/popular']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type GalleryPost = components['schemas']['GalleryPost']; + +// @public (undocumented) +type GalleryPostsCreateRequest = operations['gallery/posts/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type GalleryPostsCreateResponse = operations['gallery/posts/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type GalleryPostsDeleteRequest = operations['gallery/posts/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type GalleryPostsLikeRequest = operations['gallery/posts/like']['requestBody']['content']['application/json']; + +// @public (undocumented) +type GalleryPostsRequest = operations['gallery/posts']['requestBody']['content']['application/json']; + +// @public (undocumented) +type GalleryPostsResponse = operations['gallery/posts']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type GalleryPostsShowRequest = operations['gallery/posts/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type GalleryPostsShowResponse = operations['gallery/posts/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type GalleryPostsUnlikeRequest = operations['gallery/posts/unlike']['requestBody']['content']['application/json']; + +// @public (undocumented) +type GalleryPostsUpdateRequest = operations['gallery/posts/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type GalleryPostsUpdateResponse = operations['gallery/posts/update']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type GetAvatarDecorationsResponse = operations['get-avatar-decorations']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type Hashtag = components['schemas']['Hashtag']; + +// @public (undocumented) +type HashtagsListRequest = operations['hashtags/list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type HashtagsListResponse = operations['hashtags/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type HashtagsSearchRequest = operations['hashtags/search']['requestBody']['content']['application/json']; + +// @public (undocumented) +type HashtagsSearchResponse = operations['hashtags/search']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type HashtagsShowRequest = operations['hashtags/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type HashtagsShowResponse = operations['hashtags/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type HashtagsTrendResponse = operations['hashtags/trend']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type HashtagsUsersRequest = operations['hashtags/users']['requestBody']['content']['application/json']; + +// @public (undocumented) +type HashtagsUsersResponse = operations['hashtags/users']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type I2faDoneRequest = operations['i/2fa/done']['requestBody']['content']['application/json']; + +// @public (undocumented) +type I2faKeyDoneRequest = operations['i/2fa/key-done']['requestBody']['content']['application/json']; + +// @public (undocumented) +type I2faPasswordLessRequest = operations['i/2fa/password-less']['requestBody']['content']['application/json']; + +// @public (undocumented) +type I2faRegisterKeyRequest = operations['i/2fa/register-key']['requestBody']['content']['application/json']; + +// @public (undocumented) +type I2faRegisterRequest = operations['i/2fa/register']['requestBody']['content']['application/json']; + +// @public (undocumented) +type I2faRemoveKeyRequest = operations['i/2fa/remove-key']['requestBody']['content']['application/json']; + +// @public (undocumented) +type I2faUnregisterRequest = operations['i/2fa/unregister']['requestBody']['content']['application/json']; + +// @public (undocumented) +type I2faUpdateKeyRequest = operations['i/2fa/update-key']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IAppsRequest = operations['i/apps']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IAuthorizedAppsRequest = operations['i/authorized-apps']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IChangePasswordRequest = operations['i/change-password']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IClaimAchievementRequest = operations['i/claim-achievement']['requestBody']['content']['application/json']; // @public (undocumented) type ID = string; // @public (undocumented) -type Instance = { - id: ID; - firstRetrievedAt: DateString; - host: string; - usersCount: number; - notesCount: number; - followingCount: number; - followersCount: number; - driveUsage: number; - driveFiles: number; - latestRequestSentAt: DateString | null; - latestStatus: number | null; - latestRequestReceivedAt: DateString | null; - lastCommunicatedAt: DateString; - isNotResponding: boolean; - isSuspended: boolean; - isSilenced: boolean; - isBlocked: boolean; - softwareName: string | null; - softwareVersion: string | null; - openRegistrations: boolean | null; - name: string | null; - description: string | null; - maintainerName: string | null; - maintainerEmail: string | null; - iconUrl: string | null; - faviconUrl: string | null; - themeColor: string | null; - infoUpdatedAt: DateString | null; -}; +type IDeleteAccountRequest = operations['i/delete-account']['requestBody']['content']['application/json']; // @public (undocumented) -type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata; +type IExportFollowingRequest = operations['i/export-following']['requestBody']['content']['application/json']; // @public (undocumented) -type Invite = { - id: ID; - code: string; - expiresAt: DateString | null; - createdAt: DateString; - createdBy: UserLite | null; - usedBy: UserLite | null; - usedAt: DateString | null; - used: boolean; -}; +type IFavoritesRequest = operations['i/favorites']['requestBody']['content']['application/json']; // @public (undocumented) -type InviteLimit = { - remaining: number; -}; +type IFavoritesResponse = operations['i/favorites']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type IGalleryLikesRequest = operations['i/gallery/likes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IGalleryLikesResponse = operations['i/gallery/likes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type IGalleryPostsRequest = operations['i/gallery/posts']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IGalleryPostsResponse = operations['i/gallery/posts']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type IImportAntennasRequest = operations['i/import-antennas']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IImportBlockingRequest = operations['i/import-blocking']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IImportFollowingRequest = operations['i/import-following']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IImportMutingRequest = operations['i/import-muting']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IImportUserListsRequest = operations['i/import-user-lists']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IMoveRequest = operations['i/move']['requestBody']['content']['application/json']; + +// @public (undocumented) +type INotificationsGroupedRequest = operations['i/notifications-grouped']['requestBody']['content']['application/json']; + +// @public (undocumented) +type INotificationsGroupedResponse = operations['i/notifications-grouped']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type INotificationsRequest = operations['i/notifications']['requestBody']['content']['application/json']; + +// @public (undocumented) +type INotificationsResponse = operations['i/notifications']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type InviteCode = components['schemas']['InviteCode']; + +// @public (undocumented) +type InviteCreateResponse = operations['invite/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type InviteDeleteRequest = operations['invite/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type InviteLimitResponse = operations['invite/limit']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type InviteListRequest = operations['invite/list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type InviteListResponse = operations['invite/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type IPageLikesRequest = operations['i/page-likes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IPageLikesResponse = operations['i/page-likes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type IPagesRequest = operations['i/pages']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IPagesResponse = operations['i/pages']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type IPinRequest = operations['i/pin']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IPinResponse = operations['i/pin']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type IReadAnnouncementRequest = operations['i/read-announcement']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IRegenerateTokenRequest = operations['i/regenerate-token']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IRegistryGetAllRequest = operations['i/registry/get-all']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IRegistryGetDetailRequest = operations['i/registry/get-detail']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IRegistryGetRequest = operations['i/registry/get']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IRegistryKeysRequest = operations['i/registry/keys']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IRegistryKeysWithTypeRequest = operations['i/registry/keys-with-type']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IRegistryRemoveRequest = operations['i/registry/remove']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IRegistrySetRequest = operations['i/registry/set']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IResponse = operations['i']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type IRevokeTokenRequest = operations['i/revoke-token']['requestBody']['content']['application/json']; // @public (undocumented) function isAPIError(reason: any): reason is APIError; // @public (undocumented) -type LiteInstanceMetadata = { - maintainerName: string | null; - maintainerEmail: string | null; - version: string; - name: string | null; - shortName: string | null; - uri: string; - description: string | null; - langs: string[]; - tosUrl: string | null; - repositoryUrl: string; - feedbackUrl: string; - impressumUrl: string | null; - privacyPolicyUrl: string | null; - disableRegistration: boolean; - disableLocalTimeline: boolean; - disableGlobalTimeline: boolean; - driveCapacityPerLocalUserMb: number; - driveCapacityPerRemoteUserMb: number; - emailRequiredForSignup: boolean; - enableHcaptcha: boolean; - hcaptchaSiteKey: string | null; - enableRecaptcha: boolean; - recaptchaSiteKey: string | null; - enableTurnstile: boolean; - turnstileSiteKey: string | null; - swPublickey: string | null; - themeColor: string | null; - mascotImageUrl: string | null; - bannerUrl: string | null; - serverErrorImageUrl: string | null; - infoImageUrl: string | null; - notFoundImageUrl: string | null; - iconUrl: string | null; - backgroundImageUrl: string | null; - logoImageUrl: string | null; - maxNoteTextLength: number; - enableEmail: boolean; - enableTwitterIntegration: boolean; - enableGithubIntegration: boolean; - enableDiscordIntegration: boolean; - enableServiceWorker: boolean; - emojis: CustomEmoji[]; - defaultDarkTheme: string | null; - defaultLightTheme: string | null; - ads: { - id: ID; - ratio: number; - place: string; - url: string; - imageUrl: string; - }[]; - notesPerOneAd: number; - translatorAvailable: boolean; - serverRules: string[]; -}; +type ISigninHistoryRequest = operations['i/signin-history']['requestBody']['content']['application/json']; // @public (undocumented) -type MeDetailed = UserDetailed & { - avatarId: DriveFile['id']; - bannerId: DriveFile['id']; - autoAcceptFollowed: boolean; - alwaysMarkNsfw: boolean; - carefulBot: boolean; - emailNotificationTypes: string[]; - hasPendingReceivedFollowRequest: boolean; - hasUnreadAnnouncement: boolean; - hasUnreadAntenna: boolean; - hasUnreadMentions: boolean; - hasUnreadMessagingMessage: boolean; - hasUnreadNotification: boolean; - hasUnreadSpecifiedNotes: boolean; - unreadNotificationsCount: number; - hideOnlineStatus: boolean; - injectFeaturedNote: boolean; - integrations: Record<string, any>; - isDeleted: boolean; - isExplorable: boolean; - mutedWords: string[][]; - notificationRecieveConfig: { - [notificationType in typeof notificationTypes_2[number]]?: { - type: 'all'; - } | { - type: 'never'; - } | { - type: 'following'; - } | { - type: 'follower'; - } | { - type: 'mutualFollow'; - } | { - type: 'list'; - userListId: string; - }; - }; - noCrawle: boolean; - receiveAnnouncementEmail: boolean; - usePasswordLessLogin: boolean; - unreadAnnouncements: Announcement[]; - twoFactorBackupCodesStock: 'full' | 'partial' | 'none'; - [other: string]: any; -}; +type ISigninHistoryResponse = operations['i/signin-history']['responses']['200']['content']['application/json']; // @public (undocumented) -type MeDetailedWithSecret = MeDetailed & { - email: string; - emailVerified: boolean; - securityKeysList: { - id: string; - name: string; - lastUsed: string; - }[]; -}; +type IUnpinRequest = operations['i/unpin']['requestBody']['content']['application/json']; // @public (undocumented) -type MeSignup = MeDetailedWithSecret & { - token: string; -}; +type IUnpinResponse = operations['i/unpin']['responses']['200']['content']['application/json']; // @public (undocumented) -type MessagingMessage = { - id: ID; - createdAt: DateString; - file: DriveFile | null; - fileId: DriveFile['id'] | null; - isRead: boolean; - reads: User['id'][]; - text: string | null; - user: User; - userId: User['id']; - recipient?: User | null; - recipientId: User['id'] | null; - group?: UserGroup | null; - groupId: UserGroup['id'] | null; -}; +type IUpdateEmailRequest = operations['i/update-email']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IUpdateRequest = operations['i/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IUpdateResponse = operations['i/update']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type IWebhooksCreateRequest = operations['i/webhooks/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IWebhooksDeleteRequest = operations['i/webhooks/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IWebhooksShowRequest = operations['i/webhooks/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type IWebhooksUpdateRequest = operations['i/webhooks/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type MeDetailed = components['schemas']['MeDetailed']; + +// @public (undocumented) +type MeDetailedOnly = components['schemas']['MeDetailedOnly']; + +// @public (undocumented) +type MetaRequest = operations['meta']['requestBody']['content']['application/json']; + +// @public (undocumented) +type MetaResponse = operations['meta']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type MiauthGenTokenRequest = operations['miauth/gen-token']['requestBody']['content']['application/json']; + +// @public (undocumented) +type MiauthGenTokenResponse = operations['miauth/gen-token']['responses']['200']['content']['application/json']; // @public (undocumented) type ModerationLog = { @@ -2674,173 +2133,217 @@ type ModerationLog = { } | { type: 'resolveAbuseReport'; info: ModerationLogPayloads['resolveAbuseReport']; +} | { + type: 'unsetUserAvatar'; + info: ModerationLogPayloads['unsetUserAvatar']; +} | { + type: 'unsetUserBanner'; + info: ModerationLogPayloads['unsetUserBanner']; }); // @public (undocumented) -export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration"]; +export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"]; + +// @public (undocumented) +type MuteCreateRequest = operations['mute/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type MuteDeleteRequest = operations['mute/delete']['requestBody']['content']['application/json']; // @public (undocumented) export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"]; // @public (undocumented) -type Note = { - id: ID; - createdAt: DateString; - text: string | null; - cw: string | null; - user: User; - userId: User['id']; - reply?: Note; - replyId: Note['id']; - renote?: Note; - renoteId: Note['id']; - files: DriveFile[]; - fileIds: DriveFile['id'][]; - visibility: 'public' | 'home' | 'followers' | 'specified'; - visibleUserIds?: User['id'][]; - channel?: Channel; - channelId?: Channel['id']; - localOnly?: boolean; - myReaction?: string; - reactions: Record<string, number>; - renoteCount: number; - repliesCount: number; - clippedCount?: number; - poll?: { - expiresAt: DateString | null; - multiple: boolean; - choices: { - isVoted: boolean; - text: string; - votes: number; - }[]; - }; - emojis: { - name: string; - url: string; - }[]; - uri?: string; - url?: string; - isHidden?: boolean; -}; +type MuteListRequest = operations['mute/list']['requestBody']['content']['application/json']; // @public (undocumented) -type NoteFavorite = { - id: ID; - createdAt: DateString; - noteId: Note['id']; - note: Note; -}; +type MuteListResponse = operations['mute/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type NoteReaction = { - id: ID; - createdAt: DateString; - user: UserLite; - type: string; -}; +type Muting = components['schemas']['Muting']; + +// @public (undocumented) +type MyAppsRequest = operations['my/apps']['requestBody']['content']['application/json']; + +// @public (undocumented) +type MyAppsResponse = operations['my/apps']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type Note = components['schemas']['Note']; + +// @public (undocumented) +type NoteFavorite = components['schemas']['NoteFavorite']; + +// @public (undocumented) +type NoteReaction = components['schemas']['NoteReaction']; + +// @public (undocumented) +type NotesChildrenRequest = operations['notes/children']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesChildrenResponse = operations['notes/children']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesClipsRequest = operations['notes/clips']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesClipsResponse = operations['notes/clips']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesConversationRequest = operations['notes/conversation']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesConversationResponse = operations['notes/conversation']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesCreateRequest = operations['notes/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesCreateResponse = operations['notes/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesDeleteRequest = operations['notes/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesFavoritesCreateRequest = operations['notes/favorites/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesFavoritesDeleteRequest = operations['notes/favorites/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesFeaturedRequest = operations['notes/featured']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesFeaturedResponse = operations['notes/featured']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesGlobalTimelineRequest = operations['notes/global-timeline']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesGlobalTimelineResponse = operations['notes/global-timeline']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesHybridTimelineRequest = operations['notes/hybrid-timeline']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesHybridTimelineResponse = operations['notes/hybrid-timeline']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesLocalTimelineRequest = operations['notes/local-timeline']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesLocalTimelineResponse = operations['notes/local-timeline']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesMentionsRequest = operations['notes/mentions']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesMentionsResponse = operations['notes/mentions']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesPollsRecommendationRequest = operations['notes/polls/recommendation']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesPollsRecommendationResponse = operations['notes/polls/recommendation']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesPollsVoteRequest = operations['notes/polls/vote']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesReactionsCreateRequest = operations['notes/reactions/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesReactionsDeleteRequest = operations['notes/reactions/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesReactionsRequest = operations['notes/reactions']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesReactionsResponse = operations['notes/reactions']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesRenotesRequest = operations['notes/renotes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesRenotesResponse = operations['notes/renotes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesRepliesRequest = operations['notes/replies']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesRepliesResponse = operations['notes/replies']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesRequest = operations['notes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesResponse = operations['notes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesSearchByTagRequest = operations['notes/search-by-tag']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesSearchByTagResponse = operations['notes/search-by-tag']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesSearchRequest = operations['notes/search']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesSearchResponse = operations['notes/search']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesShowRequest = operations['notes/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesShowResponse = operations['notes/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesStateRequest = operations['notes/state']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesStateResponse = operations['notes/state']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesThreadMutingCreateRequest = operations['notes/thread-muting/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesThreadMutingDeleteRequest = operations['notes/thread-muting/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesTimelineRequest = operations['notes/timeline']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesTimelineResponse = operations['notes/timeline']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesTranslateRequest = operations['notes/translate']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesTranslateResponse = operations['notes/translate']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type NotesUnrenoteRequest = operations['notes/unrenote']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesUserListTimelineRequest = operations['notes/user-list-timeline']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesUserListTimelineResponse = operations['notes/user-list-timeline']['responses']['200']['content']['application/json']; // @public (undocumented) export const noteVisibilities: readonly ["public", "home", "followers", "specified"]; // @public (undocumented) -type Notification_2 = { - id: ID; - createdAt: DateString; - isRead: boolean; -} & ({ - type: 'reaction'; - reaction: string; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'reply'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'renote'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'quote'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'mention'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'note'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'pollEnded'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'follow'; - user: User; - userId: User['id']; -} | { - type: 'followRequestAccepted'; - user: User; - userId: User['id']; -} | { - type: 'receiveFollowRequest'; - user: User; - userId: User['id']; -} | { - type: 'groupInvited'; - invitation: UserGroup; - user: User; - userId: User['id']; -} | { - type: 'achievementEarned'; - achievement: string; -} | { - type: 'app'; - header?: string | null; - body: string; - icon?: string | null; -} | { - type: 'test'; -}); +type Notification_2 = components['schemas']['Notification']; // @public (undocumented) -export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "achievementEarned"]; +type NotificationsCreateRequest = operations['notifications/create']['requestBody']['content']['application/json']; // @public (undocumented) -type OriginType = 'combined' | 'local' | 'remote'; +export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned"]; // @public (undocumented) -type Page = { - id: ID; - createdAt: DateString; - updatedAt: DateString; - userId: User['id']; - user: User; - content: Record<string, any>[]; - variables: Record<string, any>[]; - title: string; - name: string; - summary: string | null; - hideTitleWhenPinned: boolean; - alignCenter: boolean; - font: string; - script: string; - eyeCatchingImageId: DriveFile['id'] | null; - eyeCatchingImage: DriveFile | null; - attachedFiles: any; - likedCount: number; - isLiked?: boolean; -}; +type Page = components['schemas']['Page']; // @public (undocumented) type PageEvent = { @@ -2852,46 +2355,145 @@ type PageEvent = { }; // @public (undocumented) +type PagePushRequest = operations['page-push']['requestBody']['content']['application/json']; + +// @public (undocumented) +type PagesCreateRequest = operations['pages/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type PagesCreateResponse = operations['pages/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type PagesDeleteRequest = operations['pages/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type PagesFeaturedResponse = operations['pages/featured']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type PagesLikeRequest = operations['pages/like']['requestBody']['content']['application/json']; + +// @public (undocumented) +type PagesShowRequest = operations['pages/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type PagesShowResponse = operations['pages/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type PagesUnlikeRequest = operations['pages/unlike']['requestBody']['content']['application/json']; + +// @public (undocumented) +type PagesUpdateRequest = operations['pages/update']['requestBody']['content']['application/json']; + +// @public (undocumented) function parse(acct: string): Acct; // @public (undocumented) export const permissions: string[]; // @public (undocumented) -type ServerInfo = { - machine: string; - cpu: { - model: string; - cores: number; +type PingResponse = operations['ping']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type PinnedUsersResponse = operations['pinned-users']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type PromoReadRequest = operations['promo/read']['requestBody']['content']['application/json']; + +// @public (undocumented) +type QueueCount = components['schemas']['QueueCount']; + +// @public (undocumented) +type QueueStats = { + deliver: { + activeSincePrevTick: number; + active: number; + waiting: number; + delayed: number; + }; + inbox: { + activeSincePrevTick: number; + active: number; + waiting: number; + delayed: number; }; +}; + +// @public (undocumented) +type QueueStatsLog = string[]; + +// @public (undocumented) +type RenoteMuteCreateRequest = operations['renote-mute/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type RenoteMuteDeleteRequest = operations['renote-mute/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type RenoteMuteListRequest = operations['renote-mute/list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type RenoteMuteListResponse = operations['renote-mute/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type RenoteMuting = components['schemas']['RenoteMuting']; + +// @public (undocumented) +type RequestResetPasswordRequest = operations['request-reset-password']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ResetPasswordRequest = operations['reset-password']['requestBody']['content']['application/json']; + +// @public (undocumented) +type RetentionResponse = operations['retention']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type Role = components['schemas']['Role']; + +// @public (undocumented) +type RoleLite = components['schemas']['RoleLite']; + +// @public (undocumented) +type RolesListResponse = operations['roles/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type RolesNotesRequest = operations['roles/notes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type RolesNotesResponse = operations['roles/notes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type RolesShowRequest = operations['roles/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type RolesShowResponse = operations['roles/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type RolesUsersRequest = operations['roles/users']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ServerStats = { + cpu: number; mem: { - total: number; + used: number; + active: number; + }; + net: { + rx: number; + tx: number; }; fs: { - total: number; - used: number; + r: number; + w: number; }; }; // @public (undocumented) -type Signin = { - id: ID; - createdAt: DateString; - ip: string; - headers: Record<string, any>; - success: boolean; -}; +type ServerStatsLog = string[]; // @public (undocumented) -type Stats = { - notesCount: number; - originalNotesCount: number; - usersCount: number; - originalUsersCount: number; - instances: number; - driveUsageLocal: number; - driveUsageRemote: number; -}; +type Signin = components['schemas']['Signin']; + +// @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 // @@ -2932,115 +2534,223 @@ export class Stream extends EventEmitter<StreamEvents> { useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnection<Channels[C]>; } +// Warning: (ae-forgotten-export) The symbol "SwitchCase" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "IsCaseMatched" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "GetCaseResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type SwitchCaseResponseType<E extends keyof Endpoints, P extends Endpoints[E]['req']> = Endpoints[E]['res'] extends SwitchCase ? IsCaseMatched<E, P, 0> extends true ? GetCaseResult<E, P, 0> : IsCaseMatched<E, P, 1> extends true ? GetCaseResult<E, P, 1> : IsCaseMatched<E, P, 2> extends true ? GetCaseResult<E, P, 2> : IsCaseMatched<E, P, 3> extends true ? GetCaseResult<E, P, 3> : IsCaseMatched<E, P, 4> extends true ? GetCaseResult<E, P, 4> : IsCaseMatched<E, P, 5> extends true ? GetCaseResult<E, P, 5> : IsCaseMatched<E, P, 6> extends true ? GetCaseResult<E, P, 6> : IsCaseMatched<E, P, 7> extends true ? GetCaseResult<E, P, 7> : IsCaseMatched<E, P, 8> extends true ? GetCaseResult<E, P, 8> : IsCaseMatched<E, P, 9> extends true ? GetCaseResult<E, P, 9> : Endpoints[E]['res']['$switch']['$default'] : Endpoints[E]['res']; + +// @public (undocumented) +type SwRegisterRequest = operations['sw/register']['requestBody']['content']['application/json']; + +// @public (undocumented) +type SwRegisterResponse = operations['sw/register']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type SwShowRegistrationRequest = operations['sw/show-registration']['requestBody']['content']['application/json']; + +// @public (undocumented) +type SwShowRegistrationResponse = operations['sw/show-registration']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type SwUnregisterRequest = operations['sw/unregister']['requestBody']['content']['application/json']; + +// @public (undocumented) +type SwUpdateRegistrationRequest = operations['sw/update-registration']['requestBody']['content']['application/json']; + +// @public (undocumented) +type SwUpdateRegistrationResponse = operations['sw/update-registration']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type TestRequest = operations['test']['requestBody']['content']['application/json']; + // @public (undocumented) function toString_2(acct: Acct): string; // @public (undocumented) -type User = UserLite | UserDetailed; +type User = components['schemas']['User']; // @public (undocumented) -type UserDetailed = UserLite & { - alsoKnownAs: string[]; - bannerBlurhash: string | null; - bannerColor: string | null; - bannerUrl: string | null; - birthday: string | null; - createdAt: DateString; - description: string | null; - ffVisibility: 'public' | 'followers' | 'private'; - fields: { - name: string; - value: string; - }[]; - verifiedLinks: string[]; - followersCount: number; - followingCount: number; - hasPendingFollowRequestFromYou: boolean; - hasPendingFollowRequestToYou: boolean; - isAdmin: boolean; - isBlocked: boolean; - isBlocking: boolean; - isBot: boolean; - isCat: boolean; - speakAsCat: boolean; - isFollowed: boolean; - isFollowing: boolean; - isLocked: boolean; - isModerator: boolean; - isMuted: boolean; - isSilenced: boolean; - isSuspended: boolean; - lang: string | null; - lastFetchedAt?: DateString; - location: string | null; - movedTo: string; - notesCount: number; - pinnedNoteIds: ID[]; - pinnedNotes: Note[]; - pinnedPage: Page | null; - pinnedPageId: string | null; - publicReactions: boolean; - securityKeys: boolean; - twoFactorEnabled: boolean; - updatedAt: DateString | null; - uri: string | null; - url: string | null; - notify: 'normal' | 'none'; -}; +type UserDetailed = components['schemas']['UserDetailed']; // @public (undocumented) -type UserGroup = TODO_2; +type UserDetailedNotMe = components['schemas']['UserDetailedNotMe']; // @public (undocumented) -type UserList = { - id: ID; - createdAt: DateString; - name: string; - userIds: User['id'][]; -}; +type UserDetailedNotMeOnly = components['schemas']['UserDetailedNotMeOnly']; // @public (undocumented) -type UserLite = { - id: ID; - username: string; - host: string | null; - name: string | null; - onlineStatus: 'online' | 'active' | 'offline' | 'unknown'; - avatarUrl: string; - avatarBlurhash: string; - avatarDecorations: { - id: ID; - url: string; - angle?: number; - flipH?: boolean; - }[]; - emojis: { - name: string; - url: string; - }[]; - instance?: { - name: Instance['name']; - softwareName: Instance['softwareName']; - softwareVersion: Instance['softwareVersion']; - iconUrl: Instance['iconUrl']; - faviconUrl: Instance['faviconUrl']; - themeColor: Instance['themeColor']; - }; - isCat?: boolean; - isBot?: boolean; -}; +type UserList = components['schemas']['UserList']; + +// @public (undocumented) +type UserLite = components['schemas']['UserLite']; + +// @public (undocumented) +type UsernameAvailableRequest = operations['username/available']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsernameAvailableResponse = operations['username/available']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersAchievementsRequest = operations['users/achievements']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersClipsRequest = operations['users/clips']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersClipsResponse = operations['users/clips']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersFeaturedNotesRequest = operations['users/featured-notes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersFeaturedNotesResponse = operations['users/featured-notes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersFlashsRequest = operations['users/flashs']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersFlashsResponse = operations['users/flashs']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersFollowersRequest = operations['users/followers']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersFollowersResponse = operations['users/followers']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersFollowingRequest = operations['users/following']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersFollowingResponse = operations['users/following']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersGalleryPostsRequest = operations['users/gallery/posts']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersGalleryPostsResponse = operations['users/gallery/posts']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersGetFrequentlyRepliedUsersRequest = operations['users/get-frequently-replied-users']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersGetFrequentlyRepliedUsersResponse = operations['users/get-frequently-replied-users']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersListsCreateFromPublicRequest = operations['users/lists/create-from-public']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersListsCreateFromPublicResponse = operations['users/lists/create-from-public']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersListsCreateRequest = operations['users/lists/create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersListsCreateResponse = operations['users/lists/create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersListsDeleteRequest = operations['users/lists/delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersListsFavoriteRequest = operations['users/lists/favorite']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersListsGetMembershipsRequest = operations['users/lists/get-memberships']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersListsListRequest = operations['users/lists/list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersListsListResponse = operations['users/lists/list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersListsPullRequest = operations['users/lists/pull']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersListsPushRequest = operations['users/lists/push']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersListsShowRequest = operations['users/lists/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersListsShowResponse = operations['users/lists/show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersListsUnfavoriteRequest = operations['users/lists/unfavorite']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersListsUpdateMembershipRequest = operations['users/lists/update-membership']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersListsUpdateRequest = operations['users/lists/update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersListsUpdateResponse = operations['users/lists/update']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersNotesRequest = operations['users/notes']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersNotesResponse = operations['users/notes']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersPagesRequest = operations['users/pages']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersPagesResponse = operations['users/pages']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersReactionsRequest = operations['users/reactions']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersReactionsResponse = operations['users/reactions']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersRecommendationRequest = operations['users/recommendation']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersRecommendationResponse = operations['users/recommendation']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersRelationRequest = operations['users/relation']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersRelationResponse = operations['users/relation']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersReportAbuseRequest = operations['users/report-abuse']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersRequest = operations['users']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersResponse = operations['users']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersSearchByUsernameAndHostRequest = operations['users/search-by-username-and-host']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersSearchByUsernameAndHostResponse = operations['users/search-by-username-and-host']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersSearchRequest = operations['users/search']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersSearchResponse = operations['users/search']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type UsersShowRequest = operations['users/show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type UsersShowResponse = operations['users/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt'; +type UsersUpdateMemoRequest = operations['users/update-memo']['requestBody']['content']['application/json']; // Warnings were encountered during analysis: // -// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts -// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts -// src/api.types.ts:632:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts -// src/entities.ts:116:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts -// src/entities.ts:627:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts -// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts +// src/entities.ts:25:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/generator/.eslintrc.cjs b/packages/misskey-js/generator/.eslintrc.cjs new file mode 100644 index 0000000000..6a8b31da9c --- /dev/null +++ b/packages/misskey-js/generator/.eslintrc.cjs @@ -0,0 +1,9 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + extends: [ + '../../shared/.eslintrc.js', + ], +}; diff --git a/packages/misskey-js/generator/.gitignore b/packages/misskey-js/generator/.gitignore new file mode 100644 index 0000000000..1a11577de7 --- /dev/null +++ b/packages/misskey-js/generator/.gitignore @@ -0,0 +1 @@ +api.json diff --git a/packages/misskey-js/generator/README.md b/packages/misskey-js/generator/README.md new file mode 100644 index 0000000000..767ccfa185 --- /dev/null +++ b/packages/misskey-js/generator/README.md @@ -0,0 +1,19 @@ +## misskey-js向け型生成モジュール + +バックエンドが吐き出すOpenAPI準拠のapi.jsonからmisskey-jsで使用される型エイリアスを生成するためのモジュールです。 +このモジュールはmisskey-jsそのものにバンドルされることは想定しておらず、生成物をmisskey-jsのsrc配下にコピーして使用することを想定しています。 + +## 使い方 + +まず、Misskeyのバックエンドからapi.jsonを取得する必要があります。任意のMisskeyインスタンスの/api-docからダウンロードしても良いですし、 +backendモジュール配下で`pnpm generate-api-json`を実行しても良いでしょう。 + +api.jsonを入手したら、このファイルがあるディレクトリに置いてください。 + +その後、以下コマンドを実行します。 + +```shell +pnpm generate +``` + +上記を実行することで、`./built`ディレクトリ配下にtsファイルが生成されます。 diff --git a/packages/misskey-js/generator/package.json b/packages/misskey-js/generator/package.json new file mode 100644 index 0000000000..50b23f5792 --- /dev/null +++ b/packages/misskey-js/generator/package.json @@ -0,0 +1,24 @@ +{ + "name": "misskey-js-type-generator", + "version": "0.0.0", + "description": "Misskey TypeGenerator", + "type": "module", + "scripts": { + "generate": "tsx src/generator.ts && eslint ./built/**/* --ext .ts --fix" + }, + "devDependencies": { + "@apidevtools/swagger-parser": "10.1.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" + }, + "files": [ + "built" + ] +} diff --git a/packages/misskey-js/generator/src/generator.ts b/packages/misskey-js/generator/src/generator.ts new file mode 100644 index 0000000000..f12ed94513 --- /dev/null +++ b/packages/misskey-js/generator/src/generator.ts @@ -0,0 +1,349 @@ +import { mkdir, writeFile } from 'fs/promises'; +import { OpenAPIV3 } from 'openapi-types'; +import { toPascal } from 'ts-case-convert'; +import SwaggerParser from '@apidevtools/swagger-parser'; +import openapiTS from 'openapi-typescript'; + +function generateVersionHeaderComment(openApiDocs: OpenAPIV3.Document): string { + const contents = { + version: openApiDocs.info.version, + generatedAt: new Date().toISOString(), + }; + + const lines: string[] = []; + lines.push('/*'); + for (const [key, value] of Object.entries(contents)) { + lines.push(` * ${key}: ${value}`); + } + lines.push(' */'); + + return lines.join('\n'); +} + +async function generateBaseTypes( + openApiDocs: OpenAPIV3.Document, + openApiJsonPath: string, + typeFileName: string, +) { + const disabledLints = [ + '@typescript-eslint/naming-convention', + '@typescript-eslint/no-explicit-any', + ]; + + const lines: string[] = []; + for (const lint of disabledLints) { + lines.push(`/* eslint ${lint}: 0 */`); + } + lines.push(''); + + lines.push(generateVersionHeaderComment(openApiDocs)); + lines.push(''); + + const generatedTypes = await openapiTS(openApiJsonPath, { exportType: true }); + lines.push(generatedTypes); + lines.push(''); + + await writeFile(typeFileName, lines.join('\n')); +} + +async function generateSchemaEntities( + openApiDocs: OpenAPIV3.Document, + typeFileName: string, + outputPath: string, +) { + if (!openApiDocs.components?.schemas) { + return; + } + + const schemas = openApiDocs.components.schemas; + const schemaNames = Object.keys(schemas); + const typeAliasLines: string[] = []; + + typeAliasLines.push(generateVersionHeaderComment(openApiDocs)); + typeAliasLines.push(''); + typeAliasLines.push(`import { components } from '${toImportPath(typeFileName)}';`); + typeAliasLines.push( + ...schemaNames.map(it => `export type ${it} = components['schemas']['${it}'];`), + ); + typeAliasLines.push(''); + + await writeFile(outputPath, typeAliasLines.join('\n')); +} + +async function generateEndpoints( + openApiDocs: OpenAPIV3.Document, + typeFileName: string, + entitiesOutputPath: string, + endpointOutputPath: string, +) { + const endpoints: Endpoint[] = []; + + // misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり + const paths = openApiDocs.paths; + const postPathItems = Object.keys(paths) + .map(it => paths[it]?.post) + .filter(filterUndefined); + + for (const operation of postPathItems) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const operationId = operation.operationId!; + const endpoint = new Endpoint(operationId); + endpoints.push(endpoint); + + if (isRequestBodyObject(operation.requestBody)) { + const reqContent = operation.requestBody.content; + const supportMediaTypes = Object.keys(reqContent); + if (supportMediaTypes.length > 0) { + // いまのところ複数のメディアタイプをとるエンドポイントは無いので決め打ちする + endpoint.request = new OperationTypeAlias( + operationId, + supportMediaTypes[0], + OperationsAliasType.REQUEST, + ); + } + } + + if (isResponseObject(operation.responses['200']) && operation.responses['200'].content) { + const resContent = operation.responses['200'].content; + const supportMediaTypes = Object.keys(resContent); + if (supportMediaTypes.length > 0) { + // いまのところ複数のメディアタイプを返すエンドポイントは無いので決め打ちする + endpoint.response = new OperationTypeAlias( + operationId, + supportMediaTypes[0], + OperationsAliasType.RESPONSE, + ); + } + } + } + + const entitiesOutputLine: string[] = []; + + entitiesOutputLine.push(generateVersionHeaderComment(openApiDocs)); + entitiesOutputLine.push(''); + + entitiesOutputLine.push(`import { operations } from '${toImportPath(typeFileName)}';`); + entitiesOutputLine.push(''); + + entitiesOutputLine.push(new EmptyTypeAlias(OperationsAliasType.REQUEST).toLine()); + entitiesOutputLine.push(new EmptyTypeAlias(OperationsAliasType.RESPONSE).toLine()); + entitiesOutputLine.push(''); + + const entities = endpoints + .flatMap(it => [it.request, it.response].filter(i => i)) + .filter(filterUndefined); + entitiesOutputLine.push(...entities.map(it => it.toLine())); + entitiesOutputLine.push(''); + + await writeFile(entitiesOutputPath, entitiesOutputLine.join('\n')); + + const endpointOutputLine: string[] = []; + + endpointOutputLine.push(generateVersionHeaderComment(openApiDocs)); + endpointOutputLine.push(''); + + endpointOutputLine.push('import type {'); + endpointOutputLine.push( + ...[emptyRequest, emptyResponse, ...entities].map(it => '\t' + it.generateName() + ','), + ); + endpointOutputLine.push(`} from '${toImportPath(entitiesOutputPath)}';`); + endpointOutputLine.push(''); + + endpointOutputLine.push('export type Endpoints = {'); + endpointOutputLine.push( + ...endpoints.map(it => '\t' + it.toLine()), + ); + endpointOutputLine.push('}'); + endpointOutputLine.push(''); + + await writeFile(endpointOutputPath, endpointOutputLine.join('\n')); +} + +async function generateApiClientJSDoc( + openApiDocs: OpenAPIV3.Document, + apiClientFileName: string, + endpointsFileName: string, + warningsOutputPath: string, +) { + const endpoints: { operationId: string; description: string; }[] = []; + + // misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり + const paths = openApiDocs.paths; + const postPathItems = Object.keys(paths) + .map(it => paths[it]?.post) + .filter(filterUndefined); + + for (const operation of postPathItems) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const operationId = operation.operationId!; + + if (operation.description) { + endpoints.push({ + operationId: operationId, + description: operation.description, + }); + } + } + + const endpointOutputLine: string[] = []; + + endpointOutputLine.push(generateVersionHeaderComment(openApiDocs)); + endpointOutputLine.push(''); + + endpointOutputLine.push(`import type { SwitchCaseResponseType } from '${toImportPath(apiClientFileName)}';`); + endpointOutputLine.push(`import type { Endpoints } from '${toImportPath(endpointsFileName)}';`); + endpointOutputLine.push(''); + + endpointOutputLine.push(`declare module '${toImportPath(apiClientFileName)}' {`); + endpointOutputLine.push(' export interface APIClient {'); + for (let i = 0; i < endpoints.length; i++) { + const endpoint = endpoints[i]; + + endpointOutputLine.push( + ' /**', + ` * ${endpoint.description.split('\n').join('\n * ')}`, + ' */', + ` request<E extends '${endpoint.operationId}', P extends Endpoints[E][\'req\']>(`, + ' endpoint: E,', + ' params: P,', + ' credential?: string | null,', + ' ): Promise<SwitchCaseResponseType<E, P>>;', + ); + + if (i < endpoints.length - 1) { + endpointOutputLine.push('\n'); + } + } + endpointOutputLine.push(' }'); + endpointOutputLine.push('}'); + endpointOutputLine.push(''); + + await writeFile(warningsOutputPath, endpointOutputLine.join('\n')); +} + +function isRequestBodyObject(value: unknown): value is OpenAPIV3.RequestBodyObject { + if (!value) { + return false; + } + + const { content } = value as Record<keyof OpenAPIV3.RequestBodyObject, unknown>; + return content !== undefined; +} + +function isResponseObject(value: unknown): value is OpenAPIV3.ResponseObject { + if (!value) { + return false; + } + + const { description } = value as Record<keyof OpenAPIV3.ResponseObject, unknown>; + return description !== undefined; +} + +function filterUndefined<T>(item: T): item is Exclude<T, undefined> { + return item !== undefined; +} + +function toImportPath(fileName: string, fromPath = '/built/autogen', toPath = ''): string { + return fileName.replace(fromPath, toPath).replace('.ts', '.js'); +} + +enum OperationsAliasType { + REQUEST = 'Request', + RESPONSE = 'Response' +} + +interface IOperationTypeAlias { + readonly type: OperationsAliasType + + generateName(): string + + toLine(): string +} + +class OperationTypeAlias implements IOperationTypeAlias { + public readonly operationId: string; + public readonly mediaType: string; + public readonly type: OperationsAliasType; + + constructor( + operationId: string, + mediaType: string, + type: OperationsAliasType, + ) { + this.operationId = operationId; + this.mediaType = mediaType; + this.type = type; + } + + generateName(): string { + const nameBase = this.operationId.replace(/\//g, '-'); + return toPascal(nameBase + this.type); + } + + toLine(): string { + const name = this.generateName(); + return (this.type === OperationsAliasType.REQUEST) + ? `export type ${name} = operations['${this.operationId}']['requestBody']['content']['${this.mediaType}'];` + : `export type ${name} = operations['${this.operationId}']['responses']['200']['content']['${this.mediaType}'];`; + } +} + +class EmptyTypeAlias implements IOperationTypeAlias { + readonly type: OperationsAliasType; + + constructor(type: OperationsAliasType) { + this.type = type; + } + + generateName(): string { + return 'Empty' + this.type; + } + + toLine(): string { + const name = this.generateName(); + return `export type ${name} = Record<string, unknown> | undefined;`; + } +} + +const emptyRequest = new EmptyTypeAlias(OperationsAliasType.REQUEST); +const emptyResponse = new EmptyTypeAlias(OperationsAliasType.RESPONSE); + +class Endpoint { + public readonly operationId: string; + public request?: IOperationTypeAlias; + public response?: IOperationTypeAlias; + + constructor(operationId: string) { + this.operationId = operationId; + } + + toLine(): string { + const reqName = this.request?.generateName() ?? emptyRequest.generateName(); + const resName = this.response?.generateName() ?? emptyResponse.generateName(); + + return `'${this.operationId}': { req: ${reqName}; res: ${resName} };`; + } +} + +async function main() { + const generatePath = './built/autogen'; + await mkdir(generatePath, { recursive: true }); + + const openApiJsonPath = './api.json'; + const openApiDocs = await SwaggerParser.validate(openApiJsonPath) as OpenAPIV3.Document; + + const typeFileName = './built/autogen/types.ts'; + await generateBaseTypes(openApiDocs, openApiJsonPath, typeFileName); + + const modelFileName = `${generatePath}/models.ts`; + await generateSchemaEntities(openApiDocs, typeFileName, modelFileName); + + const entitiesFileName = `${generatePath}/entities.ts`; + const endpointFileName = `${generatePath}/endpoint.ts`; + await generateEndpoints(openApiDocs, typeFileName, entitiesFileName, endpointFileName); + + const apiClientWarningFileName = `${generatePath}/apiClientJSDoc.ts`; + await generateApiClientJSDoc(openApiDocs, '../api.ts', endpointFileName, apiClientWarningFileName); +} + +main(); diff --git a/packages/misskey-js/generator/tsconfig.json b/packages/misskey-js/generator/tsconfig.json new file mode 100644 index 0000000000..c814df612e --- /dev/null +++ b/packages/misskey-js/generator/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "nodenext", + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "esModuleInterop": true, + "lib": [ + "esnext", + ] + }, + "include": [ + "src/**/*.ts", + "built/**/*.ts" + ], + "exclude": [] +} diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 0a4855874f..53d5044d68 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -6,6 +6,7 @@ "types": "./built/index.d.ts", "scripts": { "build": "tsc", + "watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"", "tsd": "tsd", "api": "pnpm api-extractor run --local --verbose", "api-prod": "pnpm api-extractor run --verbose", @@ -13,33 +14,36 @@ "typecheck": "tsc --noEmit", "lint": "pnpm typecheck && pnpm eslint", "jest": "jest --coverage --detectOpenHandles", - "test": "pnpm jest && pnpm tsd" + "test": "pnpm jest && pnpm tsd", + "update-autogen-code": "pnpm --filter misskey-js-type-generator generate && ncp generator/built/autogen src/autogen" }, "repository": { "type": "git", "url": "git+https://github.com/misskey-dev/misskey.js.git" }, "devDependencies": { - "@microsoft/api-extractor": "7.38.3", + "@microsoft/api-extractor": "7.38.5", "@swc/jest": "0.2.29", - "@types/jest": "29.5.8", - "@types/node": "20.9.1", - "@typescript-eslint/eslint-plugin": "6.11.0", - "@typescript-eslint/parser": "6.11.0", - "eslint": "8.53.0", + "@types/jest": "29.5.11", + "@types/node": "20.10.5", + "@typescript-eslint/eslint-plugin": "6.14.0", + "@typescript-eslint/parser": "6.14.0", + "eslint": "8.56.0", "jest": "29.7.0", "jest-fetch-mock": "3.0.3", "jest-websocket-mock": "2.5.0", "mock-socket": "9.3.1", - "tsd": "0.29.0", - "typescript": "5.2.2" + "ncp": "2.0.0", + "nodemon": "3.0.2", + "tsd": "0.30.0", + "typescript": "5.3.3" }, "files": [ "built" ], "dependencies": { "@swc/cli": "0.1.63", - "@swc/core": "1.3.96", + "@swc/core": "1.3.100", "eventemitter3": "5.0.1", "reconnecting-websocket": "4.4.0" } diff --git a/packages/misskey-js/src/api.ts b/packages/misskey-js/src/api.ts index 9415e692e3..0d10faaada 100644 --- a/packages/misskey-js/src/api.ts +++ b/packages/misskey-js/src/api.ts @@ -1,4 +1,11 @@ -import type { Endpoints } from './api.types.js'; +import './autogen/apiClientJSDoc'; + +import { SwitchCaseResponseType } from './api.types'; +import type { Endpoints } from './api.types'; + +export { + SwitchCaseResponseType, +} from './api.types'; const MK_API_ERROR = Symbol(); @@ -15,25 +22,15 @@ export function isAPIError(reason: any): reason is APIError { } export type FetchLike = (input: string, init?: { - method?: string; - body?: string; - credentials?: RequestCredentials; - cache?: RequestCache; - headers: {[key in string]: string} - }) => Promise<{ - status: number; - json(): Promise<any>; - }>; - -type IsNeverType<T> = [T] extends [never] ? true : false; - -type StrictExtract<Union, Cond> = Cond extends Union ? Union : never; - -type IsCaseMatched<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> = - IsNeverType<StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>> extends false ? true : false; - -type GetCaseResult<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> = - StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>[1]; + method?: string; + body?: string; + credentials?: RequestCredentials; + cache?: RequestCache; + headers: { [key in string]: string } +}) => Promise<{ + status: number; + json(): Promise<any>; +}>; export class APIClient { public origin: string; @@ -53,22 +50,11 @@ export class APIClient { } public request<E extends keyof Endpoints, P extends Endpoints[E]['req']>( - endpoint: E, params: P = {} as P, credential?: string | null | undefined, - ): Promise<Endpoints[E]['res'] extends { $switch: { $cases: [any, any][]; $default: any; }; } - ? - IsCaseMatched<E, P, 0> extends true ? GetCaseResult<E, P, 0> : - IsCaseMatched<E, P, 1> extends true ? GetCaseResult<E, P, 1> : - IsCaseMatched<E, P, 2> extends true ? GetCaseResult<E, P, 2> : - IsCaseMatched<E, P, 3> extends true ? GetCaseResult<E, P, 3> : - IsCaseMatched<E, P, 4> extends true ? GetCaseResult<E, P, 4> : - IsCaseMatched<E, P, 5> extends true ? GetCaseResult<E, P, 5> : - IsCaseMatched<E, P, 6> extends true ? GetCaseResult<E, P, 6> : - IsCaseMatched<E, P, 7> extends true ? GetCaseResult<E, P, 7> : - IsCaseMatched<E, P, 8> extends true ? GetCaseResult<E, P, 8> : - IsCaseMatched<E, P, 9> extends true ? GetCaseResult<E, P, 9> : - Endpoints[E]['res']['$switch']['$default'] - : Endpoints[E]['res']> { - const promise = new Promise((resolve, reject) => { + endpoint: E, + params: P = {} as P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>> { + return new Promise((resolve, reject) => { this.fetch(`${this.origin}/api/${endpoint}`, { method: 'POST', body: JSON.stringify({ @@ -83,10 +69,8 @@ export class APIClient { }).then(async (res) => { const body = res.status === 204 ? null : await res.json(); - if (res.status === 200) { + if (res.status === 200 || res.status === 204) { resolve(body); - } else if (res.status === 204) { - resolve(null); } else { reject({ [MK_API_ERROR]: true, @@ -95,7 +79,5 @@ export class APIClient { } }).catch(reject); }); - - return promise as any; } } diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index 9212d088ce..d97646b7cc 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -1,675 +1,60 @@ -import type { - Ad, Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, DetailedInstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, GalleryPost, Instance, - LiteInstanceMetadata, - MeDetailed, - Note, NoteFavorite, OriginType, Page, ServerInfo, Stats, User, UserDetailed, MeSignup, UserGroup, UserList, UserSorting, Notification, NoteReaction, Signin, MessagingMessage, Invite, InviteLimit, AdminInstanceMetadata, -} from './entities.js'; +import { Endpoints as Gen } from './autogen/endpoint'; +import { UserDetailed } from './autogen/models'; +import { UsersShowRequest } from './autogen/entities'; -type TODO = Record<string, any> | null; +type Overwrite<T, U extends { [Key in keyof T]?: unknown }> = Omit< + T, + keyof U +> & U; -type NoParams = Record<string, never>; - -type ShowUserReq = { username: string; host?: string; } | { userId: User['id']; }; - -export type Endpoints = { - // admin - 'admin/abuse-user-reports': { req: TODO; res: TODO; }; - 'admin/delete-all-files-of-a-user': { req: { userId: User['id']; }; res: null; }; - 'admin/delete-logs': { req: NoParams; res: null; }; - 'admin/get-index-stats': { req: TODO; res: TODO; }; - 'admin/get-table-stats': { req: TODO; res: TODO; }; - 'admin/invite': { req: TODO; res: TODO; }; - 'admin/logs': { req: TODO; res: TODO; }; - 'admin/meta': { req: NoParams; res: AdminInstanceMetadata; }; - 'admin/reset-password': { req: TODO; res: TODO; }; - 'admin/resolve-abuse-user-report': { req: TODO; res: TODO; }; - 'admin/resync-chart': { req: TODO; res: TODO; }; - 'admin/send-email': { req: TODO; res: TODO; }; - 'admin/server-info': { req: TODO; res: TODO; }; - 'admin/show-moderation-logs': { req: TODO; res: TODO; }; - 'admin/show-user': { req: TODO; res: TODO; }; - 'admin/show-users': { req: TODO; res: TODO; }; - 'admin/silence-user': { req: TODO; res: TODO; }; - 'admin/suspend-user': { req: TODO; res: TODO; }; - 'admin/nsfw-user': { req: TODO; res: TODO; }; - 'admin/unnsfw-user': { req: TODO; res: TODO; }; - 'admin/approve-user': { req: TODO; res: TODO; }; - 'admin/unsilence-user': { req: TODO; res: TODO; }; - 'admin/unsuspend-user': { req: TODO; res: TODO; }; - 'admin/update-meta': { req: TODO; res: TODO; }; - 'admin/vacuum': { req: TODO; res: TODO; }; - 'admin/accounts/create': { req: TODO; res: TODO; }; - 'admin/ad/create': { req: TODO; res: TODO; }; - 'admin/ad/delete': { req: { id: Ad['id']; }; res: null; }; - 'admin/ad/list': { req: TODO; res: TODO; }; - 'admin/ad/update': { req: TODO; res: TODO; }; - 'admin/announcements/create': { req: TODO; res: TODO; }; - 'admin/announcements/delete': { req: { id: Announcement['id'] }; res: null; }; - 'admin/announcements/list': { req: TODO; res: TODO; }; - 'admin/announcements/update': { req: TODO; res: TODO; }; - 'admin/drive/clean-remote-files': { req: TODO; res: TODO; }; - 'admin/drive/cleanup': { req: TODO; res: TODO; }; - 'admin/drive/files': { req: TODO; res: TODO; }; - 'admin/drive/show-file': { req: TODO; res: TODO; }; - 'admin/emoji/add': { req: TODO; res: TODO; }; - 'admin/emoji/copy': { req: TODO; res: TODO; }; - 'admin/emoji/list-remote': { req: TODO; res: TODO; }; - 'admin/emoji/list': { req: TODO; res: TODO; }; - 'admin/emoji/remove': { req: TODO; res: TODO; }; - 'admin/emoji/update': { req: TODO; res: TODO; }; - 'admin/federation/delete-all-files': { req: { host: string; }; res: null; }; - 'admin/federation/refresh-remote-instance-metadata': { req: TODO; res: TODO; }; - 'admin/federation/remove-all-following': { req: TODO; res: TODO; }; - 'admin/federation/update-instance': { req: TODO; res: TODO; }; - 'admin/invite/create': { req: TODO; res: TODO; }; - 'admin/invite/list': { req: TODO; res: TODO; }; - 'admin/moderators/add': { req: TODO; res: TODO; }; - 'admin/moderators/remove': { req: TODO; res: TODO; }; - 'admin/promo/create': { req: TODO; res: TODO; }; - 'admin/queue/clear': { req: TODO; res: TODO; }; - 'admin/queue/deliver-delayed': { req: TODO; res: TODO; }; - 'admin/queue/inbox-delayed': { req: TODO; res: TODO; }; - 'admin/queue/jobs': { req: TODO; res: TODO; }; - 'admin/queue/stats': { req: TODO; res: TODO; }; - 'admin/relays/add': { req: TODO; res: TODO; }; - 'admin/relays/list': { req: TODO; res: TODO; }; - 'admin/relays/remove': { req: TODO; res: TODO; }; - - // announcements - 'announcements': { req: { limit?: number; withUnreads?: boolean; sinceId?: Announcement['id']; untilId?: Announcement['id']; }; res: Announcement[]; }; - - // antennas - 'antennas/create': { req: TODO; res: Antenna; }; - 'antennas/delete': { req: { antennaId: Antenna['id']; }; res: null; }; - 'antennas/list': { req: NoParams; res: Antenna[]; }; - 'antennas/notes': { req: { antennaId: Antenna['id']; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; }; - 'antennas/show': { req: { antennaId: Antenna['id']; }; res: Antenna; }; - 'antennas/update': { req: TODO; res: Antenna; }; - - // ap - 'ap/get': { req: { uri: string; }; res: Record<string, any>; }; - 'ap/show': { req: { uri: string; }; res: { - type: 'Note'; - object: Note; - } | { - type: 'User'; - object: UserDetailed; - }; }; - - // app - 'app/create': { req: TODO; res: App; }; - 'app/show': { req: { appId: App['id']; }; res: App; }; - - // auth - 'auth/accept': { req: { token: string; }; res: null; }; - 'auth/session/generate': { req: { appSecret: string; }; res: { token: string; url: string; }; }; - 'auth/session/show': { req: { token: string; }; res: AuthSession; }; - 'auth/session/userkey': { req: { appSecret: string; token: string; }; res: { accessToken: string; user: User }; }; - - // blocking - 'blocking/create': { req: { userId: User['id'] }; res: UserDetailed; }; - 'blocking/delete': { req: { userId: User['id'] }; res: UserDetailed; }; - 'blocking/list': { req: { limit?: number; sinceId?: Blocking['id']; untilId?: Blocking['id']; }; res: Blocking[]; }; - - // channels - 'channels/create': { req: TODO; res: TODO; }; - 'channels/featured': { req: TODO; res: TODO; }; - 'channels/follow': { req: TODO; res: TODO; }; - 'channels/followed': { req: TODO; res: TODO; }; - 'channels/owned': { req: TODO; res: TODO; }; - 'channels/pin-note': { req: TODO; res: TODO; }; - 'channels/show': { req: TODO; res: TODO; }; - 'channels/timeline': { req: TODO; res: TODO; }; - 'channels/unfollow': { req: TODO; res: TODO; }; - 'channels/update': { req: TODO; res: TODO; }; - - // charts - 'charts/active-users': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: { - local: { - users: number[]; - }; - remote: { - users: number[]; - }; - }; }; - 'charts/drive': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: { - local: { - decCount: number[]; - decSize: number[]; - incCount: number[]; - incSize: number[]; - totalCount: number[]; - totalSize: number[]; - }; - remote: { - decCount: number[]; - decSize: number[]; - incCount: number[]; - incSize: number[]; - totalCount: number[]; - totalSize: number[]; - }; - }; }; - 'charts/federation': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: { - instance: { - dec: number[]; - inc: number[]; - total: number[]; - }; - }; }; - 'charts/hashtag': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: TODO; }; - 'charts/instance': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; host: string; }; res: { - drive: { - decFiles: number[]; - decUsage: number[]; - incFiles: number[]; - incUsage: number[]; - totalFiles: number[]; - totalUsage: number[]; - }; - followers: { - dec: number[]; - inc: number[]; - total: number[]; - }; - following: { - dec: number[]; - inc: number[]; - total: number[]; - }; - notes: { - dec: number[]; - inc: number[]; - total: number[]; - diffs: { - normal: number[]; - renote: number[]; - reply: number[]; - }; - }; - requests: { - failed: number[]; - received: number[]; - succeeded: number[]; - }; - users: { - dec: number[]; - inc: number[]; - total: number[]; - }; - }; }; - 'charts/network': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: TODO; }; - 'charts/notes': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: { - local: { - dec: number[]; - inc: number[]; - total: number[]; - diffs: { - normal: number[]; - renote: number[]; - reply: number[]; - }; - }; - remote: { - dec: number[]; - inc: number[]; - total: number[]; - diffs: { - normal: number[]; - renote: number[]; - reply: number[]; - }; - }; - }; }; - 'charts/user/drive': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; userId: User['id']; }; res: { - decCount: number[]; - decSize: number[]; - incCount: number[]; - incSize: number[]; - totalCount: number[]; - totalSize: number[]; - }; }; - 'charts/user/following': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; userId: User['id']; }; res: TODO; }; - 'charts/user/notes': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; userId: User['id']; }; res: { - dec: number[]; - inc: number[]; - total: number[]; - diffs: { - normal: number[]; - renote: number[]; - reply: number[]; - }; - }; }; - 'charts/user/reactions': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; userId: User['id']; }; res: TODO; }; - 'charts/users': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: { - local: { - dec: number[]; - inc: number[]; - total: number[]; - }; - remote: { - dec: number[]; - inc: number[]; - total: number[]; - }; - }; }; - - // clips - 'clips/add-note': { req: TODO; res: TODO; }; - 'clips/create': { req: TODO; res: TODO; }; - 'clips/delete': { req: { clipId: Clip['id']; }; res: null; }; - 'clips/list': { req: TODO; res: TODO; }; - 'clips/notes': { req: TODO; res: TODO; }; - 'clips/show': { req: TODO; res: TODO; }; - 'clips/update': { req: TODO; res: TODO; }; - - // drive - 'drive': { req: NoParams; res: { capacity: number; usage: number; }; }; - 'drive/files': { req: { folderId?: DriveFolder['id'] | null; type?: DriveFile['type'] | null; limit?: number; sinceId?: DriveFile['id']; untilId?: DriveFile['id']; }; res: DriveFile[]; }; - 'drive/files/attached-notes': { req: TODO; res: TODO; }; - 'drive/files/check-existence': { req: TODO; res: TODO; }; - 'drive/files/create': { - req: { - folderId?: string, - name?: string, - comment?: string, - isSentisive?: boolean, - force?: boolean, - }; - res: DriveFile; - }; - 'drive/files/delete': { req: { fileId: DriveFile['id']; }; res: null; }; - 'drive/files/find-by-hash': { req: TODO; res: TODO; }; - 'drive/files/find': { req: { name: string; folderId?: DriveFolder['id'] | null; }; res: DriveFile[]; }; - 'drive/files/show': { req: { fileId?: DriveFile['id']; url?: string; }; res: DriveFile; }; - 'drive/files/update': { req: { fileId: DriveFile['id']; folderId?: DriveFolder['id'] | null; name?: string; isSensitive?: boolean; comment?: string | null; }; res: DriveFile; }; - 'drive/files/upload-from-url': { req: { url: string; folderId?: DriveFolder['id'] | null; isSensitive?: boolean; comment?: string | null; marker?: string | null; force?: boolean; }; res: null; }; - 'drive/folders': { req: { folderId?: DriveFolder['id'] | null; limit?: number; sinceId?: DriveFile['id']; untilId?: DriveFile['id']; }; res: DriveFolder[]; }; - 'drive/folders/create': { req: { name?: string; parentId?: DriveFolder['id'] | null; }; res: DriveFolder; }; - 'drive/folders/delete': { req: { folderId: DriveFolder['id']; }; res: null; }; - 'drive/folders/find': { req: { name: string; parentId?: DriveFolder['id'] | null; }; res: DriveFolder[]; }; - 'drive/folders/show': { req: { folderId: DriveFolder['id']; }; res: DriveFolder; }; - 'drive/folders/update': { req: { folderId: DriveFolder['id']; name?: string; parentId?: DriveFolder['id'] | null; }; res: DriveFolder; }; - 'drive/stream': { req: { type?: DriveFile['type'] | null; limit?: number; sinceId?: DriveFile['id']; untilId?: DriveFile['id']; }; res: DriveFile[]; }; - - // endpoint - 'endpoint': { req: { endpoint: string; }; res: { params: { name: string; type: string; }[]; }; }; - - // endpoints - 'endpoints': { req: NoParams; res: string[]; }; - - // federation - 'federation/dns': { req: { host: string; }; res: { - a: string[]; - aaaa: string[]; - cname: string[]; - txt: string[]; - }; }; - 'federation/followers': { req: { host: string; limit?: number; sinceId?: Following['id']; untilId?: Following['id']; }; res: FollowingFolloweePopulated[]; }; - 'federation/following': { req: { host: string; limit?: number; sinceId?: Following['id']; untilId?: Following['id']; }; res: FollowingFolloweePopulated[]; }; - 'federation/instances': { req: { - host?: string | null; - blocked?: boolean | null; - notResponding?: boolean | null; - suspended?: boolean | null; - federating?: boolean | null; - subscribing?: boolean | null; - publishing?: boolean | null; - limit?: number; - offset?: number; - sort?: '+pubSub' | '-pubSub' | '+notes' | '-notes' | '+users' | '-users' | '+following' | '-following' | '+followers' | '-followers' | '+caughtAt' | '-caughtAt' | '+lastCommunicatedAt' | '-lastCommunicatedAt' | '+driveUsage' | '-driveUsage' | '+driveFiles' | '-driveFiles'; - }; res: Instance[]; }; - 'federation/show-instance': { req: { host: string; }; res: Instance; }; - 'federation/update-remote-user': { req: { userId: User['id']; }; res: null; }; - 'federation/users': { req: { host: string; limit?: number; sinceId?: User['id']; untilId?: User['id']; }; res: UserDetailed[]; }; - - // following - 'following/create': { req: { - userId: User['id'], - withReplies?: boolean, - }; res: User; }; - 'following/delete': { req: { userId: User['id'] }; res: User; }; - 'following/requests/accept': { req: { userId: User['id'] }; res: null; }; - 'following/requests/cancel': { req: { userId: User['id'] }; res: User; }; - 'following/requests/list': { req: NoParams; res: FollowRequest[]; }; - 'following/requests/reject': { req: { userId: User['id'] }; res: null; }; - - // gallery - 'gallery/featured': { req: null; res: GalleryPost[]; }; - 'gallery/popular': { req: null; res: GalleryPost[]; }; - 'gallery/posts': { req: { limit?: number; sinceId?: GalleryPost['id']; untilId?: GalleryPost['id']; }; res: GalleryPost[]; }; - 'gallery/posts/create': { req: { title: GalleryPost['title']; description?: GalleryPost['description']; fileIds: GalleryPost['fileIds']; isSensitive?: GalleryPost['isSensitive'] }; res: GalleryPost; }; - 'gallery/posts/delete': { req: { postId: GalleryPost['id'] }; res: null; }; - 'gallery/posts/like': { req: { postId: GalleryPost['id'] }; res: null; }; - 'gallery/posts/show': { req: { postId: GalleryPost['id'] }; res: GalleryPost; }; - 'gallery/posts/unlike': { req: { postId: GalleryPost['id'] }; res: null; }; - 'gallery/posts/update': { req: { postId: GalleryPost['id']; title: GalleryPost['title']; description?: GalleryPost['description']; fileIds: GalleryPost['fileIds']; isSensitive?: GalleryPost['isSensitive'] }; res: GalleryPost; }; - - // games - 'games/reversi/games': { req: TODO; res: TODO; }; - 'games/reversi/games/show': { req: TODO; res: TODO; }; - 'games/reversi/games/surrender': { req: TODO; res: TODO; }; - 'games/reversi/invitations': { req: TODO; res: TODO; }; - 'games/reversi/match': { req: TODO; res: TODO; }; - 'games/reversi/match/cancel': { req: TODO; res: TODO; }; - - // get-online-users-count - 'get-online-users-count': { req: NoParams; res: { count: number; }; }; - - // hashtags - 'hashtags/list': { req: TODO; res: TODO; }; - 'hashtags/search': { req: TODO; res: TODO; }; - 'hashtags/show': { req: TODO; res: TODO; }; - 'hashtags/trend': { req: TODO; res: TODO; }; - 'hashtags/users': { req: TODO; res: TODO; }; - - // i - 'i': { req: NoParams; res: User; }; - 'i/apps': { req: TODO; res: TODO; }; - 'i/authorized-apps': { req: TODO; res: TODO; }; - 'i/change-password': { req: TODO; res: TODO; }; - 'i/delete-account': { req: { password: string; }; res: null; }; - 'i/export-blocking': { req: TODO; res: TODO; }; - 'i/export-following': { req: TODO; res: TODO; }; - 'i/export-mute': { req: TODO; res: TODO; }; - 'i/export-notes': { req: TODO; res: TODO; }; - 'i/export-user-lists': { req: TODO; res: TODO; }; - 'i/favorites': { req: { limit?: number; sinceId?: NoteFavorite['id']; untilId?: NoteFavorite['id']; }; res: NoteFavorite[]; }; - 'i/gallery/likes': { req: TODO; res: TODO; }; - 'i/gallery/posts': { req: TODO; res: TODO; }; - 'i/import-following': { req: TODO; res: TODO; }; - 'i/import-user-lists': { req: TODO; res: TODO; }; - 'i/move': { req: TODO; res: TODO; }; - 'i/notifications': { req: { - limit?: number; - sinceId?: Notification['id']; - untilId?: Notification['id']; - following?: boolean; - markAsRead?: boolean; - includeTypes?: Notification['type'][]; - excludeTypes?: Notification['type'][]; - }; res: Notification[]; }; - 'i/page-likes': { req: TODO; res: TODO; }; - 'i/pages': { req: TODO; res: TODO; }; - 'i/pin': { req: { noteId: Note['id']; }; res: MeDetailed; }; - 'i/read-all-messaging-messages': { req: TODO; res: TODO; }; - 'i/read-all-unread-notes': { req: TODO; res: TODO; }; - 'i/read-announcement': { req: TODO; res: TODO; }; - 'i/regenerate-token': { req: { password: string; }; res: null; }; - 'i/registry/get-all': { req: { scope?: string[]; }; res: Record<string, any>; }; - 'i/registry/get-detail': { req: { key: string; scope?: string[]; }; res: { updatedAt: DateString; value: any; }; }; - 'i/registry/get': { req: { key: string; scope?: string[]; }; res: any; }; - 'i/registry/keys-with-type': { req: { scope?: string[]; }; res: Record<string, 'null' | 'array' | 'number' | 'string' | 'boolean' | 'object'>; }; - 'i/registry/keys': { req: { scope?: string[]; }; res: string[]; }; - 'i/registry/remove': { req: { key: string; scope?: string[]; }; res: null; }; - 'i/registry/set': { req: { key: string; value: any; scope?: string[]; }; res: null; }; - 'i/revoke-token': { req: TODO; res: TODO; }; - 'i/signin-history': { req: { limit?: number; sinceId?: Signin['id']; untilId?: Signin['id']; }; res: Signin[]; }; - 'i/unpin': { req: { noteId: Note['id']; }; res: MeDetailed; }; - 'i/update-email': { req: { - password: string; - email?: string | null; - }; res: MeDetailed; }; - 'i/update': { req: { - name?: string | null; - description?: string | null; - lang?: string | null; - location?: string | null; - birthday?: string | null; - avatarId?: DriveFile['id'] | null; - bannerId?: DriveFile['id'] | null; - backgroundId?: DriveFile['id'] | null; - fields?: { - name: string; - value: string; - }[]; - isLocked?: boolean; - isExplorable?: boolean; - hideOnlineStatus?: boolean; - carefulBot?: boolean; - autoAcceptFollowed?: boolean; - noCrawle?: boolean; - isBot?: boolean; - isCat?: boolean; - speakAsCat?: boolean; - injectFeaturedNote?: boolean; - receiveAnnouncementEmail?: boolean; - alwaysMarkNsfw?: boolean; - mutedWords?: string[][]; - notificationRecieveConfig?: any; - emailNotificationTypes?: string[]; - alsoKnownAs?: string[]; - }; res: MeDetailed; }; - 'i/user-group-invites': { req: TODO; res: TODO; }; - 'i/2fa/done': { req: TODO; res: TODO; }; - 'i/2fa/key-done': { req: TODO; res: TODO; }; - 'i/2fa/password-less': { req: TODO; res: TODO; }; - 'i/2fa/register-key': { req: TODO; res: TODO; }; - 'i/2fa/register': { req: TODO; res: TODO; }; - 'i/2fa/remove-key': { req: TODO; res: TODO; }; - 'i/2fa/unregister': { req: TODO; res: TODO; }; - - // invite - 'invite/create': { req: NoParams; res: Invite; }; - 'invite/delete': { req: { inviteId: Invite['id']; }; res: null; }; - 'invite/list': { req: { limit?: number; sinceId?: Invite['id']; untilId?: Invite['id'] }; res: Invite[]; }; - 'invite/limit': { req: NoParams; res: InviteLimit; }; - - // messaging - 'messaging/history': { req: { limit?: number; group?: boolean; }; res: MessagingMessage[]; }; - 'messaging/messages': { req: { userId?: User['id']; groupId?: UserGroup['id']; limit?: number; sinceId?: MessagingMessage['id']; untilId?: MessagingMessage['id']; markAsRead?: boolean; }; res: MessagingMessage[]; }; - 'messaging/messages/create': { req: { userId?: User['id']; groupId?: UserGroup['id']; text?: string; fileId?: DriveFile['id']; }; res: MessagingMessage; }; - 'messaging/messages/delete': { req: { messageId: MessagingMessage['id']; }; res: null; }; - 'messaging/messages/read': { req: { messageId: MessagingMessage['id']; }; res: null; }; - - // meta - 'meta': { req: { detail?: boolean; }; res: { - $switch: { - $cases: [[ - { detail: true; }, - DetailedInstanceMetadata, - ], [ - { detail: false; }, - LiteInstanceMetadata, - ], [ - { detail: boolean; }, - LiteInstanceMetadata | DetailedInstanceMetadata, - ]]; - $default: LiteInstanceMetadata; - }; - }; }; - - // miauth - 'miauth/gen-token': { req: TODO; res: TODO; }; - - // mute - 'mute/create': { req: TODO; res: TODO; }; - 'mute/delete': { req: { userId: User['id'] }; res: null; }; - 'mute/list': { req: TODO; res: TODO; }; - - // my - 'my/apps': { req: TODO; res: TODO; }; - - // notes - 'notes': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; }; - 'notes/children': { req: { noteId: Note['id']; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; }; - 'notes/clips': { req: TODO; res: TODO; }; - 'notes/conversation': { req: TODO; res: TODO; }; - 'notes/create': { req: { - visibility?: 'public' | 'home' | 'followers' | 'specified', - visibleUserIds?: User['id'][]; - text?: null | string; - cw?: null | string; - viaMobile?: boolean; - localOnly?: boolean; - fileIds?: DriveFile['id'][]; - replyId?: null | Note['id']; - renoteId?: null | Note['id']; - channelId?: null | Channel['id']; - poll?: null | { - choices: string[]; - multiple?: boolean; - expiresAt?: null | number; - expiredAfter?: null | number; - }; - }; res: { createdNote: Note }; }; - 'notes/delete': { req: { noteId: Note['id']; }; res: null; }; - 'notes/edit': { req: { - visibility?: 'public' | 'home' | 'followers' | 'specified', - visibleUserIds?: User['id'][]; - text?: null | string; - cw?: null | string; - viaMobile?: boolean; - localOnly?: boolean; - fileIds?: DriveFile['id'][]; - replyId?: null | Note['id']; - renoteId?: null | Note['id']; - channelId?: null | Channel['id']; - poll?: null | { - choices: string[]; - multiple?: boolean; - expiresAt?: null | number; - expiredAfter?: null | number; - }; - }; res: { createdNote: Note }; }; - 'notes/favorites/create': { req: { noteId: Note['id']; }; res: null; }; - 'notes/favorites/delete': { req: { noteId: Note['id']; }; res: null; }; - 'notes/featured': { req: TODO; res: Note[]; }; - 'notes/global-timeline': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; }; - 'notes/hybrid-timeline': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; }; - 'notes/local-timeline': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; }; - 'notes/mentions': { req: { following?: boolean; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; }; - 'notes/polls/recommendation': { req: TODO; res: TODO; }; - 'notes/polls/vote': { req: { noteId: Note['id']; choice: number; }; res: null; }; - 'notes/reactions': { req: { noteId: Note['id']; type?: string | null; limit?: number; }; res: NoteReaction[]; }; - 'notes/reactions/create': { req: { noteId: Note['id']; reaction: string; }; res: null; }; - 'notes/reactions/delete': { req: { noteId: Note['id']; }; res: null; }; - 'notes/like': { req: { noteId: Note['id']; override: string | null; }; res: null; }; - 'notes/renotes': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; noteId: Note['id']; }; res: Note[]; }; - 'notes/replies': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; noteId: Note['id']; }; res: Note[]; }; - 'notes/search-by-tag': { req: TODO; res: TODO; }; - 'notes/search': { req: TODO; res: TODO; }; - 'notes/show': { req: { noteId: Note['id']; }; res: Note; }; - 'notes/state': { req: TODO; res: TODO; }; - 'notes/timeline': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; }; - 'notes/unrenote': { req: { noteId: Note['id']; }; res: null; }; - 'notes/user-list-timeline': { req: { listId: UserList['id']; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; }; - 'notes/watching/create': { req: TODO; res: TODO; }; - 'notes/watching/delete': { req: { noteId: Note['id']; }; res: null; }; - - // notifications - 'notifications/create': { req: { body: string; header?: string | null; icon?: string | null; }; res: null; }; - 'notifications/test-notification': { req: NoParams; res: null; }; - 'notifications/mark-all-as-read': { req: NoParams; res: null; }; - - // page-push - 'page-push': { req: { pageId: Page['id']; event: string; var?: any; }; res: null; }; - - // pages - 'pages/create': { req: TODO; res: Page; }; - 'pages/delete': { req: { pageId: Page['id']; }; res: null; }; - 'pages/featured': { req: NoParams; res: Page[]; }; - 'pages/like': { req: { pageId: Page['id']; }; res: null; }; - 'pages/show': { req: { pageId?: Page['id']; name?: string; username?: string; }; res: Page; }; - 'pages/unlike': { req: { pageId: Page['id']; }; res: null; }; - 'pages/update': { req: TODO; res: null; }; - - // ping - 'ping': { req: NoParams; res: { pong: number; }; }; - - // pinned-users - 'pinned-users': { req: TODO; res: TODO; }; - - // promo - 'promo/read': { req: TODO; res: TODO; }; - - // request-reset-password - 'request-reset-password': { req: { username: string; email: string; }; res: null; }; - - // reset-password - 'reset-password': { req: { token: string; password: string; }; res: null; }; - - // sponsors - 'sponsors': { req: { forceUpdate: boolean; }; res: null; }; - - // room - 'room/show': { req: TODO; res: TODO; }; - 'room/update': { req: TODO; res: TODO; }; - - // signup - 'signup': { - req: { - username: string; - password: string; - host?: string; - invitationCode?: string; - emailAddress?: string; - 'hcaptcha-response'?: string; - 'g-recaptcha-response'?: string; - 'turnstile-response'?: string; - }; - res: MeSignup | null; +type SwitchCase = { + $switch: { + $cases: [any, any][], + $default: any; }; +}; - // stats - 'stats': { req: NoParams; res: Stats; }; - - // server-info - 'server-info': { req: NoParams; res: ServerInfo; }; +type IsNeverType<T> = [T] extends [never] ? true : false; +type StrictExtract<Union, Cond> = Cond extends Union ? Union : never; - // sw - 'sw/register': { req: TODO; res: TODO; }; +type IsCaseMatched<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> = + Endpoints[E]['res'] extends SwitchCase + ? IsNeverType<StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>> extends false ? true : false + : false - // username - 'username/available': { req: { username: string; }; res: { available: boolean; }; }; +type GetCaseResult<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> = + Endpoints[E]['res'] extends SwitchCase + ? StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>[1] + : never - // users - 'users': { req: { limit?: number; offset?: number; sort?: UserSorting; origin?: OriginType; }; res: User[]; }; - 'users/clips': { req: TODO; res: TODO; }; - 'users/followers': { req: { userId?: User['id']; username?: User['username']; host?: User['host'] | null; limit?: number; sinceId?: Following['id']; untilId?: Following['id']; }; res: FollowingFollowerPopulated[]; }; - 'users/following': { req: { userId?: User['id']; username?: User['username']; host?: User['host'] | null; limit?: number; sinceId?: Following['id']; untilId?: Following['id']; }; res: FollowingFolloweePopulated[]; }; - 'users/gallery/posts': { req: TODO; res: TODO; }; - 'users/get-frequently-replied-users': { req: TODO; res: TODO; }; - 'users/groups/create': { req: TODO; res: TODO; }; - 'users/groups/delete': { req: { groupId: UserGroup['id'] }; res: null; }; - 'users/groups/invitations/accept': { req: TODO; res: TODO; }; - 'users/groups/invitations/reject': { req: TODO; res: TODO; }; - 'users/groups/invite': { req: TODO; res: TODO; }; - 'users/groups/joined': { req: TODO; res: TODO; }; - 'users/groups/owned': { req: TODO; res: TODO; }; - 'users/groups/pull': { req: TODO; res: TODO; }; - 'users/groups/show': { req: TODO; res: TODO; }; - 'users/groups/transfer': { req: TODO; res: TODO; }; - 'users/groups/update': { req: TODO; res: TODO; }; - 'users/lists/create': { req: { name: string; }; res: UserList; }; - 'users/lists/delete': { req: { listId: UserList['id']; }; res: null; }; - 'users/lists/list': { req: NoParams; res: UserList[]; }; - 'users/lists/pull': { req: { listId: UserList['id']; userId: User['id']; }; res: null; }; - 'users/lists/push': { req: { listId: UserList['id']; userId: User['id']; }; res: null; }; - 'users/lists/show': { req: { listId: UserList['id']; }; res: UserList; }; - 'users/lists/update': { req: { listId: UserList['id']; name: string; }; res: UserList; }; - 'users/notes': { req: { userId: User['id']; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; }; - 'users/pages': { req: TODO; res: TODO; }; - 'users/flashs': { req: TODO; res: TODO; }; - 'users/recommendation': { req: TODO; res: TODO; }; - 'users/relation': { req: TODO; res: TODO; }; - 'users/report-abuse': { req: TODO; res: TODO; }; - 'users/search-by-username-and-host': { req: TODO; res: TODO; }; - 'users/search': { req: TODO; res: TODO; }; - 'users/show': { req: ShowUserReq | { userIds: User['id'][]; }; res: { - $switch: { - $cases: [[ - { userIds: User['id'][]; }, - UserDetailed[], - ]]; - $default: UserDetailed; - }; - }; }; +export type SwitchCaseResponseType<E extends keyof Endpoints, P extends Endpoints[E]['req']> = Endpoints[E]['res'] extends SwitchCase + ? IsCaseMatched<E, P, 0> extends true ? GetCaseResult<E, P, 0> : + IsCaseMatched<E, P, 1> extends true ? GetCaseResult<E, P, 1> : + IsCaseMatched<E, P, 2> extends true ? GetCaseResult<E, P, 2> : + IsCaseMatched<E, P, 3> extends true ? GetCaseResult<E, P, 3> : + IsCaseMatched<E, P, 4> extends true ? GetCaseResult<E, P, 4> : + IsCaseMatched<E, P, 5> extends true ? GetCaseResult<E, P, 5> : + IsCaseMatched<E, P, 6> extends true ? GetCaseResult<E, P, 6> : + IsCaseMatched<E, P, 7> extends true ? GetCaseResult<E, P, 7> : + IsCaseMatched<E, P, 8> extends true ? GetCaseResult<E, P, 8> : + IsCaseMatched<E, P, 9> extends true ? GetCaseResult<E, P, 9> : + Endpoints[E]['res']['$switch']['$default'] : Endpoints[E]['res']; - // fetching external data - 'fetch-rss': { req: { url: string; }; res: TODO; }; - 'fetch-external-resources': { - req: { url: string; hash: string; }; - res: { type: string; data: string; }; - }; -}; +export type Endpoints = Overwrite< + Gen, + { + 'users/show': { + req: UsersShowRequest; + res: { + $switch: { + $cases: [[ + { + userIds?: string[]; + }, UserDetailed[], + ]]; + $default: UserDetailed; + }; + }; + } + } +> diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts new file mode 100644 index 0000000000..7d58dcb5c8 --- /dev/null +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -0,0 +1,3973 @@ +/* + * version: 2023.11.0-beta.3 + * generatedAt: 2023-12-08T04:57:48.424Z + */ + +import type { SwitchCaseResponseType } from '../api.js'; +import type { Endpoints } from './endpoint.js'; + +declare module '../api.js' { + export interface APIClient { + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/meta', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/abuse-user-reports', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'admin/accounts/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/accounts/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/accounts/find-by-email', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/ad/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/ad/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/ad/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/ad/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/announcements/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/announcements/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/announcements/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/announcements/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/avatar-decorations/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/avatar-decorations/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/avatar-decorations/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/avatar-decorations/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/delete-all-files-of-a-user', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/unset-user-avatar', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/unset-user-banner', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/drive/clean-remote-files', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/drive/cleanup', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/drive/files', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/drive/show-file', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/emoji/add-aliases-bulk', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/emoji/add', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/emoji/copy', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/emoji/delete-bulk', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/emoji/delete', 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 'admin/emoji/import-zip', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/emoji/list-remote', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/emoji/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/emoji/remove-aliases-bulk', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/emoji/set-aliases-bulk', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/emoji/set-category-bulk', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/emoji/set-license-bulk', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/emoji/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/federation/delete-all-files', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/federation/refresh-remote-instance-metadata', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/federation/remove-all-following', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/federation/update-instance', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/get-index-stats', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/get-table-stats', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/get-user-ips', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/invite/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/invite/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/promo/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/queue/clear', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/queue/deliver-delayed', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/queue/inbox-delayed', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/queue/promote', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/queue/stats', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/relays/add', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/relays/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/relays/remove', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/reset-password', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/resolve-abuse-user-report', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/send-email', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/server-info', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/show-moderation-logs', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/show-user', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/show-users', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/suspend-user', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/unsuspend-user', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/update-meta', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/delete-account', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/update-user-note', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/roles/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/roles/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/roles/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/roles/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/roles/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/roles/assign', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/roles/unassign', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'admin/roles/update-default-policies', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'admin/roles/users', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'announcements', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'antennas/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'antennas/delete', 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 'antennas/list', 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 'antennas/notes', 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 'antennas/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'antennas/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'ap/get', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'ap/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'app/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'app/show', 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 'auth/accept', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'auth/session/generate', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'auth/session/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'auth/session/userkey', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:blocks* + */ + request<E extends 'blocking/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:blocks* + */ + request<E extends 'blocking/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:blocks* + */ + request<E extends 'blocking/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + request<E extends 'channels/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'channels/featured', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + request<E extends 'channels/follow', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:channels* + */ + request<E extends 'channels/followed', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:channels* + */ + request<E extends 'channels/owned', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'channels/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'channels/timeline', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + request<E extends 'channels/unfollow', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + request<E extends 'channels/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + request<E extends 'channels/favorite', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + request<E extends 'channels/unfavorite', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:channels* + */ + request<E extends 'channels/my-favorites', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'channels/search', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'charts/active-users', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'charts/ap-request', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'charts/drive', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'charts/federation', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'charts/instance', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'charts/notes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'charts/user/drive', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'charts/user/following', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'charts/user/notes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'charts/user/pv', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'charts/user/reactions', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'charts/users', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'clips/add-note', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'clips/remove-note', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'clips/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'clips/delete', 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 'clips/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + request<E extends 'clips/notes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + request<E extends 'clips/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'clips/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:clip-favorite* + */ + request<E extends 'clips/favorite', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:clip-favorite* + */ + request<E extends 'clips/unfavorite', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:clip-favorite* + */ + request<E extends 'clips/my-favorites', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request<E extends 'drive', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request<E extends 'drive/files', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Find the notes to which the given file is attached. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request<E extends 'drive/files/attached-notes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Check if a given file exists. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request<E extends 'drive/files/check-existence', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Upload a new drive file. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + request<E extends 'drive/files/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Delete an existing drive file. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + request<E extends 'drive/files/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Search for a drive file by a hash of the contents. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request<E extends 'drive/files/find-by-hash', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Search for a drive file by the given parameters. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request<E extends 'drive/files/find', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show the properties of a drive file. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request<E extends 'drive/files/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Update the properties of a drive file. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + request<E extends 'drive/files/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Request the server to download a new drive file from the specified URL. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + request<E extends 'drive/files/upload-from-url', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request<E extends 'drive/folders', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + request<E extends 'drive/folders/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + request<E extends 'drive/folders/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request<E extends 'drive/folders/find', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request<E extends 'drive/folders/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + request<E extends 'drive/folders/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request<E extends 'drive/stream', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'email-address/available', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'endpoint', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'endpoints', 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 'export-custom-emojis', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'federation/followers', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'federation/following', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'federation/instances', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'federation/show-instance', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'federation/update-remote-user', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'federation/users', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'federation/stats', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + request<E extends 'following/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + request<E extends 'following/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + request<E extends 'following/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + request<E extends 'following/update-all', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + request<E extends 'following/invalidate', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + request<E extends 'following/requests/accept', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + request<E extends 'following/requests/cancel', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:following* + */ + request<E extends 'following/requests/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + request<E extends 'following/requests/reject', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'gallery/featured', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'gallery/popular', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'gallery/posts', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery* + */ + request<E extends 'gallery/posts/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery* + */ + request<E extends 'gallery/posts/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery-likes* + */ + request<E extends 'gallery/posts/like', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'gallery/posts/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery-likes* + */ + request<E extends 'gallery/posts/unlike', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery* + */ + request<E extends 'gallery/posts/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'get-online-users-count', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'get-avatar-decorations', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'hashtags/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'hashtags/search', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'hashtags/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'hashtags/trend', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'hashtags/users', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'i', 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/2fa/done', 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/2fa/key-done', 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/2fa/password-less', 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/2fa/register-key', 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/2fa/register', 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/2fa/update-key', 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/2fa/remove-key', 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/2fa/unregister', 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/apps', 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/authorized-apps', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'i/claim-achievement', 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/change-password', 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/delete-account', 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-blocking', 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-following', 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-mute', 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-notes', 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, + 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-user-lists', 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-antennas', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:favorites* + */ + request<E extends 'i/favorites', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:gallery-likes* + */ + request<E extends 'i/gallery/likes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:gallery* + */ + request<E extends 'i/gallery/posts', 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/import-blocking', 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/import-following', 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/import-muting', 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/import-user-lists', 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/import-antennas', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:notifications* + */ + request<E extends 'i/notifications', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:notifications* + */ + request<E extends 'i/notifications-grouped', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:page-likes* + */ + request<E extends 'i/page-likes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:pages* + */ + request<E extends 'i/pages', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'i/pin', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'i/read-all-unread-notes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'i/read-announcement', 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/regenerate-token', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'i/registry/get-all', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'i/registry/get-detail', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'i/registry/get', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'i/registry/keys-with-type', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'i/registry/keys', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'i/registry/remove', 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/registry/scopes-with-domain', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'i/registry/set', 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/revoke-token', 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/signin-history', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'i/unpin', 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/update-email', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'i/update', 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/move', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'i/webhooks/create', 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/webhooks/list', 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/webhooks/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'i/webhooks/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'i/webhooks/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'invite/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'invite/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'invite/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'invite/limit', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'meta', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'emojis', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'emoji', 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 'miauth/gen-token', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:mutes* + */ + request<E extends 'mute/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:mutes* + */ + request<E extends 'mute/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:mutes* + */ + request<E extends 'mute/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:mutes* + */ + request<E extends 'renote-mute/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:mutes* + */ + request<E extends 'renote-mute/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:mutes* + */ + request<E extends 'renote-mute/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'my/apps', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes/children', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes/clips', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes/conversation', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + request<E extends 'notes/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + request<E extends 'notes/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:favorites* + */ + request<E extends 'notes/favorites/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:favorites* + */ + request<E extends 'notes/favorites/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes/featured', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes/global-timeline', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'notes/hybrid-timeline', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes/local-timeline', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'notes/mentions', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'notes/polls/recommendation', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:votes* + */ + request<E extends 'notes/polls/vote', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes/reactions', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:reactions* + */ + request<E extends 'notes/reactions/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:reactions* + */ + request<E extends 'notes/reactions/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes/renotes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes/replies', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes/search-by-tag', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes/search', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'notes/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'notes/state', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'notes/thread-muting/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'notes/thread-muting/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'notes/timeline', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'notes/translate', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + request<E extends 'notes/unrenote', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'notes/user-list-timeline', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notifications* + */ + request<E extends 'notifications/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notifications* + */ + request<E extends 'notifications/mark-all-as-read', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notifications* + */ + request<E extends 'notifications/test-notification', 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 'page-push', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:pages* + */ + request<E extends 'pages/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:pages* + */ + request<E extends 'pages/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'pages/featured', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:page-likes* + */ + request<E extends 'pages/like', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'pages/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:page-likes* + */ + request<E extends 'pages/unlike', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:pages* + */ + request<E extends 'pages/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash* + */ + request<E extends 'flash/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash* + */ + request<E extends 'flash/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'flash/featured', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash-likes* + */ + request<E extends 'flash/like', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'flash/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash-likes* + */ + request<E extends 'flash/unlike', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash* + */ + request<E extends 'flash/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:flash* + */ + request<E extends 'flash/my', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:flash-likes* + */ + request<E extends 'flash/my-likes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'ping', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'pinned-users', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'promo/read', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'roles/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'roles/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'roles/users', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'roles/notes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Request a users password to be reset. + * + * **Credential required**: *No* + */ + request<E extends 'request-reset-password', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Only available when running with <code>NODE_ENV=testing</code>. Reset the database and flush Redis. + * + * **Credential required**: *No* + */ + request<E extends 'reset-db', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Complete the password reset that was previously requested. + * + * **Credential required**: *No* + */ + request<E extends 'reset-password', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'server-info', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'stats', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Check push notification registration exists. + * + * **Credential required**: *Yes* + */ + request<E extends 'sw/show-registration', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Update push notification registration. + * + * **Credential required**: *Yes* + */ + request<E extends 'sw/update-registration', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Register to receive push notifications. + * + * **Credential required**: *Yes* + */ + request<E extends 'sw/register', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Unregister from receiving push notifications. + * + * **Credential required**: *No* + */ + request<E extends 'sw/unregister', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Endpoint for testing input validation. + * + * **Credential required**: *No* + */ + request<E extends 'test', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'username/available', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'users', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show all clips this user owns. + * + * **Credential required**: *No* + */ + request<E extends 'users/clips', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show everyone that follows this user. + * + * **Credential required**: *No* + */ + request<E extends 'users/followers', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show everyone that this user is following. + * + * **Credential required**: *No* + */ + request<E extends 'users/following', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show all gallery posts by the given user. + * + * **Credential required**: *No* + */ + request<E extends 'users/gallery/posts', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Get a list of other users that the specified user frequently replies to. + * + * **Credential required**: *No* + */ + request<E extends 'users/get-frequently-replied-users', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'users/featured-notes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Create a new list of users. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'users/lists/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Delete an existing list of users. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'users/lists/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show all lists that the authenticated user has created. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + request<E extends 'users/lists/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Remove a user from a list. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'users/lists/pull', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Add a user to an existing list. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'users/lists/push', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show the properties of a list. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + request<E extends 'users/lists/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'users/lists/favorite', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'users/lists/unfavorite', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Update the properties of a list. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'users/lists/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'users/lists/create-from-public', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'users/lists/update-membership', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + request<E extends 'users/lists/get-memberships', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'users/notes', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show all pages this user created. + * + * **Credential required**: *No* + */ + request<E extends 'users/pages', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show all flashs this user created. + * + * **Credential required**: *No* + */ + request<E extends 'users/flashs', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show all reactions this user made. + * + * **Credential required**: *No* + */ + request<E extends 'users/reactions', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show users that the authenticated user might be interested to follow. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + request<E extends 'users/recommendation', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show the different kinds of relations between the authenticated user and the specified user(s). + * + * **Credential required**: *Yes* + */ + request<E extends 'users/relation', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * File a report. + * + * **Credential required**: *Yes* + */ + request<E extends 'users/report-abuse', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Search for a user by username and/or host. + * + * **Credential required**: *No* + */ + request<E extends 'users/search-by-username-and-host', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Search for users. + * + * **Credential required**: *No* + */ + request<E extends 'users/search', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * Show the properties of a user. + * + * **Credential required**: *No* + */ + request<E extends 'users/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'users/achievements', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request<E extends 'users/update-memo', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'fetch-rss', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request<E extends 'fetch-external-resources', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'retention', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + } +} diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts new file mode 100644 index 0000000000..5efe582434 --- /dev/null +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -0,0 +1,871 @@ +/* + * version: 2023.11.0-beta.3 + * generatedAt: 2023-12-08T04:57:48.415Z + */ + +import type { + EmptyRequest, + EmptyResponse, + AdminMetaResponse, + AdminAbuseUserReportsRequest, + AdminAbuseUserReportsResponse, + AdminAccountsCreateRequest, + AdminAccountsCreateResponse, + AdminAccountsDeleteRequest, + AdminAccountsFindByEmailRequest, + AdminAdCreateRequest, + AdminAdDeleteRequest, + AdminAdListRequest, + AdminAdUpdateRequest, + AdminAnnouncementsCreateRequest, + AdminAnnouncementsCreateResponse, + AdminAnnouncementsDeleteRequest, + AdminAnnouncementsListRequest, + AdminAnnouncementsListResponse, + AdminAnnouncementsUpdateRequest, + AdminAvatarDecorationsCreateRequest, + AdminAvatarDecorationsDeleteRequest, + AdminAvatarDecorationsListRequest, + AdminAvatarDecorationsListResponse, + AdminAvatarDecorationsUpdateRequest, + AdminDeleteAllFilesOfAUserRequest, + AdminUnsetUserAvatarRequest, + AdminUnsetUserBannerRequest, + AdminDriveFilesRequest, + AdminDriveFilesResponse, + AdminDriveShowFileRequest, + AdminDriveShowFileResponse, + AdminEmojiAddAliasesBulkRequest, + AdminEmojiAddRequest, + AdminEmojiCopyRequest, + AdminEmojiCopyResponse, + AdminEmojiDeleteBulkRequest, + AdminEmojiDeleteRequest, + AdminEmojiImportZipRequest, + AdminEmojiListRemoteRequest, + AdminEmojiListRemoteResponse, + AdminEmojiListRequest, + AdminEmojiListResponse, + AdminEmojiRemoveAliasesBulkRequest, + AdminEmojiSetAliasesBulkRequest, + AdminEmojiSetCategoryBulkRequest, + AdminEmojiSetLicenseBulkRequest, + AdminEmojiUpdateRequest, + AdminFederationDeleteAllFilesRequest, + AdminFederationRefreshRemoteInstanceMetadataRequest, + AdminFederationRemoveAllFollowingRequest, + AdminFederationUpdateInstanceRequest, + AdminGetTableStatsResponse, + AdminGetUserIpsRequest, + AdminInviteCreateRequest, + AdminInviteCreateResponse, + AdminInviteListRequest, + AdminInviteListResponse, + AdminPromoCreateRequest, + AdminQueueDeliverDelayedResponse, + AdminQueueInboxDelayedResponse, + AdminQueuePromoteRequest, + AdminQueueStatsResponse, + AdminRelaysAddRequest, + AdminRelaysAddResponse, + AdminRelaysListResponse, + AdminRelaysRemoveRequest, + AdminResetPasswordRequest, + AdminResetPasswordResponse, + AdminResolveAbuseUserReportRequest, + AdminSendEmailRequest, + AdminServerInfoResponse, + AdminShowModerationLogsRequest, + AdminShowModerationLogsResponse, + AdminShowUserRequest, + AdminShowUserResponse, + AdminShowUsersRequest, + AdminShowUsersResponse, + AdminSuspendUserRequest, + AdminUnsuspendUserRequest, + AdminUpdateMetaRequest, + AdminDeleteAccountRequest, + AdminDeleteAccountResponse, + AdminUpdateUserNoteRequest, + AdminRolesCreateRequest, + AdminRolesCreateResponse, + AdminRolesDeleteRequest, + AdminRolesListResponse, + AdminRolesShowRequest, + AdminRolesShowResponse, + AdminRolesUpdateRequest, + AdminRolesAssignRequest, + AdminRolesUnassignRequest, + AdminRolesUpdateDefaultPoliciesRequest, + AdminRolesUsersRequest, + AnnouncementsRequest, + AnnouncementsResponse, + AntennasCreateRequest, + AntennasCreateResponse, + AntennasDeleteRequest, + AntennasListResponse, + AntennasNotesRequest, + AntennasNotesResponse, + AntennasShowRequest, + AntennasShowResponse, + AntennasUpdateRequest, + AntennasUpdateResponse, + ApGetRequest, + ApGetResponse, + ApShowRequest, + ApShowResponse, + AppCreateRequest, + AppCreateResponse, + AppShowRequest, + AppShowResponse, + AuthAcceptRequest, + AuthSessionGenerateRequest, + AuthSessionGenerateResponse, + AuthSessionShowRequest, + AuthSessionShowResponse, + AuthSessionUserkeyRequest, + AuthSessionUserkeyResponse, + BlockingCreateRequest, + BlockingCreateResponse, + BlockingDeleteRequest, + BlockingDeleteResponse, + BlockingListRequest, + BlockingListResponse, + ChannelsCreateRequest, + ChannelsCreateResponse, + ChannelsFeaturedResponse, + ChannelsFollowRequest, + ChannelsFollowedRequest, + ChannelsFollowedResponse, + ChannelsOwnedRequest, + ChannelsOwnedResponse, + ChannelsShowRequest, + ChannelsShowResponse, + ChannelsTimelineRequest, + ChannelsTimelineResponse, + ChannelsUnfollowRequest, + ChannelsUpdateRequest, + ChannelsUpdateResponse, + ChannelsFavoriteRequest, + ChannelsUnfavoriteRequest, + ChannelsMyFavoritesResponse, + ChannelsSearchRequest, + ChannelsSearchResponse, + ChartsActiveUsersRequest, + ChartsActiveUsersResponse, + ChartsApRequestRequest, + ChartsApRequestResponse, + ChartsDriveRequest, + ChartsDriveResponse, + ChartsFederationRequest, + ChartsFederationResponse, + ChartsInstanceRequest, + ChartsInstanceResponse, + ChartsNotesRequest, + ChartsNotesResponse, + ChartsUserDriveRequest, + ChartsUserDriveResponse, + ChartsUserFollowingRequest, + ChartsUserFollowingResponse, + ChartsUserNotesRequest, + ChartsUserNotesResponse, + ChartsUserPvRequest, + ChartsUserPvResponse, + ChartsUserReactionsRequest, + ChartsUserReactionsResponse, + ChartsUsersRequest, + ChartsUsersResponse, + ClipsAddNoteRequest, + ClipsRemoveNoteRequest, + ClipsCreateRequest, + ClipsCreateResponse, + ClipsDeleteRequest, + ClipsListResponse, + ClipsNotesRequest, + ClipsNotesResponse, + ClipsShowRequest, + ClipsShowResponse, + ClipsUpdateRequest, + ClipsUpdateResponse, + ClipsFavoriteRequest, + ClipsUnfavoriteRequest, + ClipsMyFavoritesResponse, + DriveResponse, + DriveFilesRequest, + DriveFilesResponse, + DriveFilesAttachedNotesRequest, + DriveFilesAttachedNotesResponse, + DriveFilesCheckExistenceRequest, + DriveFilesCheckExistenceResponse, + DriveFilesCreateRequest, + DriveFilesCreateResponse, + DriveFilesDeleteRequest, + DriveFilesFindByHashRequest, + DriveFilesFindByHashResponse, + DriveFilesFindRequest, + DriveFilesFindResponse, + DriveFilesShowRequest, + DriveFilesShowResponse, + DriveFilesUpdateRequest, + DriveFilesUpdateResponse, + DriveFilesUploadFromUrlRequest, + DriveFoldersRequest, + DriveFoldersResponse, + DriveFoldersCreateRequest, + DriveFoldersCreateResponse, + DriveFoldersDeleteRequest, + DriveFoldersFindRequest, + DriveFoldersFindResponse, + DriveFoldersShowRequest, + DriveFoldersShowResponse, + DriveFoldersUpdateRequest, + DriveFoldersUpdateResponse, + DriveStreamRequest, + DriveStreamResponse, + EmailAddressAvailableRequest, + EmailAddressAvailableResponse, + EndpointRequest, + EndpointsResponse, + FederationFollowersRequest, + FederationFollowersResponse, + FederationFollowingRequest, + FederationFollowingResponse, + FederationInstancesRequest, + FederationInstancesResponse, + FederationShowInstanceRequest, + FederationShowInstanceResponse, + FederationUpdateRemoteUserRequest, + FederationUsersRequest, + FederationUsersResponse, + FederationStatsRequest, + FollowingCreateRequest, + FollowingCreateResponse, + FollowingDeleteRequest, + FollowingDeleteResponse, + FollowingUpdateRequest, + FollowingUpdateResponse, + FollowingUpdateAllRequest, + FollowingInvalidateRequest, + FollowingInvalidateResponse, + FollowingRequestsAcceptRequest, + FollowingRequestsCancelRequest, + FollowingRequestsCancelResponse, + FollowingRequestsListRequest, + FollowingRequestsListResponse, + FollowingRequestsRejectRequest, + GalleryFeaturedRequest, + GalleryFeaturedResponse, + GalleryPopularResponse, + GalleryPostsRequest, + GalleryPostsResponse, + GalleryPostsCreateRequest, + GalleryPostsCreateResponse, + GalleryPostsDeleteRequest, + GalleryPostsLikeRequest, + GalleryPostsShowRequest, + GalleryPostsShowResponse, + GalleryPostsUnlikeRequest, + GalleryPostsUpdateRequest, + GalleryPostsUpdateResponse, + GetAvatarDecorationsResponse, + HashtagsListRequest, + HashtagsListResponse, + HashtagsSearchRequest, + HashtagsSearchResponse, + HashtagsShowRequest, + HashtagsShowResponse, + HashtagsTrendResponse, + HashtagsUsersRequest, + HashtagsUsersResponse, + IResponse, + I2faDoneRequest, + I2faKeyDoneRequest, + I2faPasswordLessRequest, + I2faRegisterKeyRequest, + I2faRegisterRequest, + I2faUpdateKeyRequest, + I2faRemoveKeyRequest, + I2faUnregisterRequest, + IAppsRequest, + IAuthorizedAppsRequest, + IClaimAchievementRequest, + IChangePasswordRequest, + IDeleteAccountRequest, + IExportFollowingRequest, + IFavoritesRequest, + IFavoritesResponse, + IGalleryLikesRequest, + IGalleryLikesResponse, + IGalleryPostsRequest, + IGalleryPostsResponse, + IImportBlockingRequest, + IImportFollowingRequest, + IImportMutingRequest, + IImportUserListsRequest, + IImportAntennasRequest, + INotificationsRequest, + INotificationsResponse, + INotificationsGroupedRequest, + INotificationsGroupedResponse, + IPageLikesRequest, + IPageLikesResponse, + IPagesRequest, + IPagesResponse, + IPinRequest, + IPinResponse, + IReadAnnouncementRequest, + IRegenerateTokenRequest, + IRegistryGetAllRequest, + IRegistryGetDetailRequest, + IRegistryGetRequest, + IRegistryKeysWithTypeRequest, + IRegistryKeysRequest, + IRegistryRemoveRequest, + IRegistrySetRequest, + IRevokeTokenRequest, + ISigninHistoryRequest, + ISigninHistoryResponse, + IUnpinRequest, + IUnpinResponse, + IUpdateEmailRequest, + IUpdateRequest, + IUpdateResponse, + IMoveRequest, + IWebhooksCreateRequest, + IWebhooksShowRequest, + IWebhooksUpdateRequest, + IWebhooksDeleteRequest, + InviteCreateResponse, + InviteDeleteRequest, + InviteListRequest, + InviteListResponse, + InviteLimitResponse, + MetaRequest, + MetaResponse, + EmojisResponse, + EmojiRequest, + EmojiResponse, + MiauthGenTokenRequest, + MiauthGenTokenResponse, + MuteCreateRequest, + MuteDeleteRequest, + MuteListRequest, + MuteListResponse, + RenoteMuteCreateRequest, + RenoteMuteDeleteRequest, + RenoteMuteListRequest, + RenoteMuteListResponse, + MyAppsRequest, + MyAppsResponse, + NotesRequest, + NotesResponse, + NotesChildrenRequest, + NotesChildrenResponse, + NotesClipsRequest, + NotesClipsResponse, + NotesConversationRequest, + NotesConversationResponse, + NotesCreateRequest, + NotesCreateResponse, + NotesDeleteRequest, + NotesFavoritesCreateRequest, + NotesFavoritesDeleteRequest, + NotesFeaturedRequest, + NotesFeaturedResponse, + NotesGlobalTimelineRequest, + NotesGlobalTimelineResponse, + NotesHybridTimelineRequest, + NotesHybridTimelineResponse, + NotesLocalTimelineRequest, + NotesLocalTimelineResponse, + NotesMentionsRequest, + NotesMentionsResponse, + NotesPollsRecommendationRequest, + NotesPollsRecommendationResponse, + NotesPollsVoteRequest, + NotesReactionsRequest, + NotesReactionsResponse, + NotesReactionsCreateRequest, + NotesReactionsDeleteRequest, + NotesRenotesRequest, + NotesRenotesResponse, + NotesRepliesRequest, + NotesRepliesResponse, + NotesSearchByTagRequest, + NotesSearchByTagResponse, + NotesSearchRequest, + NotesSearchResponse, + NotesShowRequest, + NotesShowResponse, + NotesStateRequest, + NotesStateResponse, + NotesThreadMutingCreateRequest, + NotesThreadMutingDeleteRequest, + NotesTimelineRequest, + NotesTimelineResponse, + NotesTranslateRequest, + NotesTranslateResponse, + NotesUnrenoteRequest, + NotesUserListTimelineRequest, + NotesUserListTimelineResponse, + NotificationsCreateRequest, + PagePushRequest, + PagesCreateRequest, + PagesCreateResponse, + PagesDeleteRequest, + PagesFeaturedResponse, + PagesLikeRequest, + PagesShowRequest, + PagesShowResponse, + PagesUnlikeRequest, + PagesUpdateRequest, + FlashCreateRequest, + FlashDeleteRequest, + FlashFeaturedResponse, + FlashLikeRequest, + FlashShowRequest, + FlashShowResponse, + FlashUnlikeRequest, + FlashUpdateRequest, + FlashMyRequest, + FlashMyResponse, + FlashMyLikesRequest, + FlashMyLikesResponse, + PingResponse, + PinnedUsersResponse, + PromoReadRequest, + RolesListResponse, + RolesShowRequest, + RolesShowResponse, + RolesUsersRequest, + RolesNotesRequest, + RolesNotesResponse, + RequestResetPasswordRequest, + ResetPasswordRequest, + StatsResponse, + SwShowRegistrationRequest, + SwShowRegistrationResponse, + SwUpdateRegistrationRequest, + SwUpdateRegistrationResponse, + SwRegisterRequest, + SwRegisterResponse, + SwUnregisterRequest, + TestRequest, + UsernameAvailableRequest, + UsernameAvailableResponse, + UsersRequest, + UsersResponse, + UsersClipsRequest, + UsersClipsResponse, + UsersFollowersRequest, + UsersFollowersResponse, + UsersFollowingRequest, + UsersFollowingResponse, + UsersGalleryPostsRequest, + UsersGalleryPostsResponse, + UsersGetFrequentlyRepliedUsersRequest, + UsersGetFrequentlyRepliedUsersResponse, + UsersFeaturedNotesRequest, + UsersFeaturedNotesResponse, + UsersListsCreateRequest, + UsersListsCreateResponse, + UsersListsDeleteRequest, + UsersListsListRequest, + UsersListsListResponse, + UsersListsPullRequest, + UsersListsPushRequest, + UsersListsShowRequest, + UsersListsShowResponse, + UsersListsFavoriteRequest, + UsersListsUnfavoriteRequest, + UsersListsUpdateRequest, + UsersListsUpdateResponse, + UsersListsCreateFromPublicRequest, + UsersListsCreateFromPublicResponse, + UsersListsUpdateMembershipRequest, + UsersListsGetMembershipsRequest, + UsersNotesRequest, + UsersNotesResponse, + UsersPagesRequest, + UsersPagesResponse, + UsersFlashsRequest, + UsersFlashsResponse, + UsersReactionsRequest, + UsersReactionsResponse, + UsersRecommendationRequest, + UsersRecommendationResponse, + UsersRelationRequest, + UsersRelationResponse, + UsersReportAbuseRequest, + UsersSearchByUsernameAndHostRequest, + UsersSearchByUsernameAndHostResponse, + UsersSearchRequest, + UsersSearchResponse, + UsersShowRequest, + UsersShowResponse, + UsersAchievementsRequest, + UsersUpdateMemoRequest, + FetchRssRequest, + FetchExternalResourcesRequest, + RetentionResponse, +} from './entities.js'; + +export type Endpoints = { + 'admin/meta': { req: EmptyRequest; res: AdminMetaResponse }; + 'admin/abuse-user-reports': { req: AdminAbuseUserReportsRequest; res: AdminAbuseUserReportsResponse }; + 'admin/accounts/create': { req: AdminAccountsCreateRequest; res: AdminAccountsCreateResponse }; + 'admin/accounts/delete': { req: AdminAccountsDeleteRequest; res: EmptyResponse }; + 'admin/accounts/find-by-email': { req: AdminAccountsFindByEmailRequest; res: EmptyResponse }; + 'admin/ad/create': { req: AdminAdCreateRequest; res: EmptyResponse }; + 'admin/ad/delete': { req: AdminAdDeleteRequest; res: EmptyResponse }; + 'admin/ad/list': { req: AdminAdListRequest; res: EmptyResponse }; + 'admin/ad/update': { req: AdminAdUpdateRequest; res: EmptyResponse }; + 'admin/announcements/create': { req: AdminAnnouncementsCreateRequest; res: AdminAnnouncementsCreateResponse }; + 'admin/announcements/delete': { req: AdminAnnouncementsDeleteRequest; res: EmptyResponse }; + 'admin/announcements/list': { req: AdminAnnouncementsListRequest; res: AdminAnnouncementsListResponse }; + 'admin/announcements/update': { req: AdminAnnouncementsUpdateRequest; res: EmptyResponse }; + 'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: EmptyResponse }; + 'admin/avatar-decorations/delete': { req: AdminAvatarDecorationsDeleteRequest; res: EmptyResponse }; + 'admin/avatar-decorations/list': { req: AdminAvatarDecorationsListRequest; res: AdminAvatarDecorationsListResponse }; + 'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse }; + 'admin/delete-all-files-of-a-user': { req: AdminDeleteAllFilesOfAUserRequest; res: EmptyResponse }; + 'admin/unset-user-avatar': { req: AdminUnsetUserAvatarRequest; res: EmptyResponse }; + 'admin/unset-user-banner': { req: AdminUnsetUserBannerRequest; res: EmptyResponse }; + 'admin/drive/clean-remote-files': { req: EmptyRequest; res: EmptyResponse }; + 'admin/drive/cleanup': { req: EmptyRequest; res: EmptyResponse }; + 'admin/drive/files': { req: AdminDriveFilesRequest; res: AdminDriveFilesResponse }; + 'admin/drive/show-file': { req: AdminDriveShowFileRequest; res: AdminDriveShowFileResponse }; + 'admin/emoji/add-aliases-bulk': { req: AdminEmojiAddAliasesBulkRequest; res: EmptyResponse }; + 'admin/emoji/add': { req: AdminEmojiAddRequest; res: EmptyResponse }; + 'admin/emoji/copy': { req: AdminEmojiCopyRequest; res: AdminEmojiCopyResponse }; + 'admin/emoji/delete-bulk': { req: AdminEmojiDeleteBulkRequest; res: EmptyResponse }; + 'admin/emoji/delete': { req: AdminEmojiDeleteRequest; res: EmptyResponse }; + 'admin/emoji/import-zip': { req: AdminEmojiImportZipRequest; res: EmptyResponse }; + 'admin/emoji/list-remote': { req: AdminEmojiListRemoteRequest; res: AdminEmojiListRemoteResponse }; + 'admin/emoji/list': { req: AdminEmojiListRequest; res: AdminEmojiListResponse }; + 'admin/emoji/remove-aliases-bulk': { req: AdminEmojiRemoveAliasesBulkRequest; res: EmptyResponse }; + 'admin/emoji/set-aliases-bulk': { req: AdminEmojiSetAliasesBulkRequest; res: EmptyResponse }; + 'admin/emoji/set-category-bulk': { req: AdminEmojiSetCategoryBulkRequest; res: EmptyResponse }; + 'admin/emoji/set-license-bulk': { req: AdminEmojiSetLicenseBulkRequest; res: EmptyResponse }; + 'admin/emoji/update': { req: AdminEmojiUpdateRequest; res: EmptyResponse }; + 'admin/federation/delete-all-files': { req: AdminFederationDeleteAllFilesRequest; res: EmptyResponse }; + 'admin/federation/refresh-remote-instance-metadata': { req: AdminFederationRefreshRemoteInstanceMetadataRequest; res: EmptyResponse }; + 'admin/federation/remove-all-following': { req: AdminFederationRemoveAllFollowingRequest; res: EmptyResponse }; + 'admin/federation/update-instance': { req: AdminFederationUpdateInstanceRequest; res: EmptyResponse }; + 'admin/get-index-stats': { req: EmptyRequest; res: EmptyResponse }; + 'admin/get-table-stats': { req: EmptyRequest; res: AdminGetTableStatsResponse }; + 'admin/get-user-ips': { req: AdminGetUserIpsRequest; res: EmptyResponse }; + 'admin/invite/create': { req: AdminInviteCreateRequest; res: AdminInviteCreateResponse }; + 'admin/invite/list': { req: AdminInviteListRequest; res: AdminInviteListResponse }; + 'admin/promo/create': { req: AdminPromoCreateRequest; res: EmptyResponse }; + 'admin/queue/clear': { req: EmptyRequest; res: EmptyResponse }; + 'admin/queue/deliver-delayed': { req: EmptyRequest; res: AdminQueueDeliverDelayedResponse }; + 'admin/queue/inbox-delayed': { req: EmptyRequest; res: AdminQueueInboxDelayedResponse }; + 'admin/queue/promote': { req: AdminQueuePromoteRequest; res: EmptyResponse }; + 'admin/queue/stats': { req: EmptyRequest; res: AdminQueueStatsResponse }; + 'admin/relays/add': { req: AdminRelaysAddRequest; res: AdminRelaysAddResponse }; + 'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse }; + 'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse }; + 'admin/reset-password': { req: AdminResetPasswordRequest; res: AdminResetPasswordResponse }; + 'admin/resolve-abuse-user-report': { req: AdminResolveAbuseUserReportRequest; res: EmptyResponse }; + 'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse }; + 'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse }; + 'admin/show-moderation-logs': { req: AdminShowModerationLogsRequest; res: AdminShowModerationLogsResponse }; + 'admin/show-user': { req: AdminShowUserRequest; res: AdminShowUserResponse }; + 'admin/show-users': { req: AdminShowUsersRequest; res: AdminShowUsersResponse }; + 'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse }; + 'admin/unsuspend-user': { req: AdminUnsuspendUserRequest; res: EmptyResponse }; + 'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse }; + 'admin/delete-account': { req: AdminDeleteAccountRequest; res: AdminDeleteAccountResponse }; + 'admin/update-user-note': { req: AdminUpdateUserNoteRequest; res: EmptyResponse }; + 'admin/roles/create': { req: AdminRolesCreateRequest; res: AdminRolesCreateResponse }; + 'admin/roles/delete': { req: AdminRolesDeleteRequest; res: EmptyResponse }; + 'admin/roles/list': { req: EmptyRequest; res: AdminRolesListResponse }; + 'admin/roles/show': { req: AdminRolesShowRequest; res: AdminRolesShowResponse }; + 'admin/roles/update': { req: AdminRolesUpdateRequest; res: EmptyResponse }; + 'admin/roles/assign': { req: AdminRolesAssignRequest; res: EmptyResponse }; + 'admin/roles/unassign': { req: AdminRolesUnassignRequest; res: EmptyResponse }; + 'admin/roles/update-default-policies': { req: AdminRolesUpdateDefaultPoliciesRequest; res: EmptyResponse }; + 'admin/roles/users': { req: AdminRolesUsersRequest; res: EmptyResponse }; + 'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse }; + 'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse }; + 'antennas/delete': { req: AntennasDeleteRequest; res: EmptyResponse }; + 'antennas/list': { req: EmptyRequest; res: AntennasListResponse }; + 'antennas/notes': { req: AntennasNotesRequest; res: AntennasNotesResponse }; + 'antennas/show': { req: AntennasShowRequest; res: AntennasShowResponse }; + 'antennas/update': { req: AntennasUpdateRequest; res: AntennasUpdateResponse }; + 'ap/get': { req: ApGetRequest; res: ApGetResponse }; + 'ap/show': { req: ApShowRequest; res: ApShowResponse }; + 'app/create': { req: AppCreateRequest; res: AppCreateResponse }; + 'app/show': { req: AppShowRequest; res: AppShowResponse }; + 'auth/accept': { req: AuthAcceptRequest; res: EmptyResponse }; + 'auth/session/generate': { req: AuthSessionGenerateRequest; res: AuthSessionGenerateResponse }; + 'auth/session/show': { req: AuthSessionShowRequest; res: AuthSessionShowResponse }; + 'auth/session/userkey': { req: AuthSessionUserkeyRequest; res: AuthSessionUserkeyResponse }; + 'blocking/create': { req: BlockingCreateRequest; res: BlockingCreateResponse }; + 'blocking/delete': { req: BlockingDeleteRequest; res: BlockingDeleteResponse }; + 'blocking/list': { req: BlockingListRequest; res: BlockingListResponse }; + 'channels/create': { req: ChannelsCreateRequest; res: ChannelsCreateResponse }; + 'channels/featured': { req: EmptyRequest; res: ChannelsFeaturedResponse }; + 'channels/follow': { req: ChannelsFollowRequest; res: EmptyResponse }; + 'channels/followed': { req: ChannelsFollowedRequest; res: ChannelsFollowedResponse }; + 'channels/owned': { req: ChannelsOwnedRequest; res: ChannelsOwnedResponse }; + 'channels/show': { req: ChannelsShowRequest; res: ChannelsShowResponse }; + 'channels/timeline': { req: ChannelsTimelineRequest; res: ChannelsTimelineResponse }; + 'channels/unfollow': { req: ChannelsUnfollowRequest; res: EmptyResponse }; + 'channels/update': { req: ChannelsUpdateRequest; res: ChannelsUpdateResponse }; + 'channels/favorite': { req: ChannelsFavoriteRequest; res: EmptyResponse }; + 'channels/unfavorite': { req: ChannelsUnfavoriteRequest; res: EmptyResponse }; + 'channels/my-favorites': { req: EmptyRequest; res: ChannelsMyFavoritesResponse }; + 'channels/search': { req: ChannelsSearchRequest; res: ChannelsSearchResponse }; + 'charts/active-users': { req: ChartsActiveUsersRequest; res: ChartsActiveUsersResponse }; + 'charts/ap-request': { req: ChartsApRequestRequest; res: ChartsApRequestResponse }; + 'charts/drive': { req: ChartsDriveRequest; res: ChartsDriveResponse }; + 'charts/federation': { req: ChartsFederationRequest; res: ChartsFederationResponse }; + 'charts/instance': { req: ChartsInstanceRequest; res: ChartsInstanceResponse }; + 'charts/notes': { req: ChartsNotesRequest; res: ChartsNotesResponse }; + 'charts/user/drive': { req: ChartsUserDriveRequest; res: ChartsUserDriveResponse }; + 'charts/user/following': { req: ChartsUserFollowingRequest; res: ChartsUserFollowingResponse }; + 'charts/user/notes': { req: ChartsUserNotesRequest; res: ChartsUserNotesResponse }; + 'charts/user/pv': { req: ChartsUserPvRequest; res: ChartsUserPvResponse }; + 'charts/user/reactions': { req: ChartsUserReactionsRequest; res: ChartsUserReactionsResponse }; + 'charts/users': { req: ChartsUsersRequest; res: ChartsUsersResponse }; + 'clips/add-note': { req: ClipsAddNoteRequest; res: EmptyResponse }; + 'clips/remove-note': { req: ClipsRemoveNoteRequest; res: EmptyResponse }; + 'clips/create': { req: ClipsCreateRequest; res: ClipsCreateResponse }; + 'clips/delete': { req: ClipsDeleteRequest; res: EmptyResponse }; + 'clips/list': { req: EmptyRequest; res: ClipsListResponse }; + 'clips/notes': { req: ClipsNotesRequest; res: ClipsNotesResponse }; + 'clips/show': { req: ClipsShowRequest; res: ClipsShowResponse }; + 'clips/update': { req: ClipsUpdateRequest; res: ClipsUpdateResponse }; + 'clips/favorite': { req: ClipsFavoriteRequest; res: EmptyResponse }; + 'clips/unfavorite': { req: ClipsUnfavoriteRequest; res: EmptyResponse }; + 'clips/my-favorites': { req: EmptyRequest; res: ClipsMyFavoritesResponse }; + 'drive': { req: EmptyRequest; res: DriveResponse }; + 'drive/files': { req: DriveFilesRequest; res: DriveFilesResponse }; + 'drive/files/attached-notes': { req: DriveFilesAttachedNotesRequest; res: DriveFilesAttachedNotesResponse }; + 'drive/files/check-existence': { req: DriveFilesCheckExistenceRequest; res: DriveFilesCheckExistenceResponse }; + 'drive/files/create': { req: DriveFilesCreateRequest; res: DriveFilesCreateResponse }; + 'drive/files/delete': { req: DriveFilesDeleteRequest; res: EmptyResponse }; + 'drive/files/find-by-hash': { req: DriveFilesFindByHashRequest; res: DriveFilesFindByHashResponse }; + 'drive/files/find': { req: DriveFilesFindRequest; res: DriveFilesFindResponse }; + 'drive/files/show': { req: DriveFilesShowRequest; res: DriveFilesShowResponse }; + 'drive/files/update': { req: DriveFilesUpdateRequest; res: DriveFilesUpdateResponse }; + 'drive/files/upload-from-url': { req: DriveFilesUploadFromUrlRequest; res: EmptyResponse }; + 'drive/folders': { req: DriveFoldersRequest; res: DriveFoldersResponse }; + 'drive/folders/create': { req: DriveFoldersCreateRequest; res: DriveFoldersCreateResponse }; + 'drive/folders/delete': { req: DriveFoldersDeleteRequest; res: EmptyResponse }; + 'drive/folders/find': { req: DriveFoldersFindRequest; res: DriveFoldersFindResponse }; + 'drive/folders/show': { req: DriveFoldersShowRequest; res: DriveFoldersShowResponse }; + 'drive/folders/update': { req: DriveFoldersUpdateRequest; res: DriveFoldersUpdateResponse }; + 'drive/stream': { req: DriveStreamRequest; res: DriveStreamResponse }; + 'email-address/available': { req: EmailAddressAvailableRequest; res: EmailAddressAvailableResponse }; + 'endpoint': { req: EndpointRequest; res: EmptyResponse }; + 'endpoints': { req: EmptyRequest; res: EndpointsResponse }; + 'export-custom-emojis': { req: EmptyRequest; res: EmptyResponse }; + 'federation/followers': { req: FederationFollowersRequest; res: FederationFollowersResponse }; + 'federation/following': { req: FederationFollowingRequest; res: FederationFollowingResponse }; + 'federation/instances': { req: FederationInstancesRequest; res: FederationInstancesResponse }; + 'federation/show-instance': { req: FederationShowInstanceRequest; res: FederationShowInstanceResponse }; + 'federation/update-remote-user': { req: FederationUpdateRemoteUserRequest; res: EmptyResponse }; + 'federation/users': { req: FederationUsersRequest; res: FederationUsersResponse }; + 'federation/stats': { req: FederationStatsRequest; res: EmptyResponse }; + 'following/create': { req: FollowingCreateRequest; res: FollowingCreateResponse }; + 'following/delete': { req: FollowingDeleteRequest; res: FollowingDeleteResponse }; + 'following/update': { req: FollowingUpdateRequest; res: FollowingUpdateResponse }; + 'following/update-all': { req: FollowingUpdateAllRequest; res: EmptyResponse }; + 'following/invalidate': { req: FollowingInvalidateRequest; res: FollowingInvalidateResponse }; + 'following/requests/accept': { req: FollowingRequestsAcceptRequest; res: EmptyResponse }; + 'following/requests/cancel': { req: FollowingRequestsCancelRequest; res: FollowingRequestsCancelResponse }; + 'following/requests/list': { req: FollowingRequestsListRequest; res: FollowingRequestsListResponse }; + 'following/requests/reject': { req: FollowingRequestsRejectRequest; res: EmptyResponse }; + 'gallery/featured': { req: GalleryFeaturedRequest; res: GalleryFeaturedResponse }; + 'gallery/popular': { req: EmptyRequest; res: GalleryPopularResponse }; + 'gallery/posts': { req: GalleryPostsRequest; res: GalleryPostsResponse }; + 'gallery/posts/create': { req: GalleryPostsCreateRequest; res: GalleryPostsCreateResponse }; + 'gallery/posts/delete': { req: GalleryPostsDeleteRequest; res: EmptyResponse }; + 'gallery/posts/like': { req: GalleryPostsLikeRequest; res: EmptyResponse }; + 'gallery/posts/show': { req: GalleryPostsShowRequest; res: GalleryPostsShowResponse }; + 'gallery/posts/unlike': { req: GalleryPostsUnlikeRequest; res: EmptyResponse }; + 'gallery/posts/update': { req: GalleryPostsUpdateRequest; res: GalleryPostsUpdateResponse }; + 'get-online-users-count': { req: EmptyRequest; res: EmptyResponse }; + 'get-avatar-decorations': { req: EmptyRequest; res: GetAvatarDecorationsResponse }; + 'hashtags/list': { req: HashtagsListRequest; res: HashtagsListResponse }; + 'hashtags/search': { req: HashtagsSearchRequest; res: HashtagsSearchResponse }; + 'hashtags/show': { req: HashtagsShowRequest; res: HashtagsShowResponse }; + 'hashtags/trend': { req: EmptyRequest; res: HashtagsTrendResponse }; + 'hashtags/users': { req: HashtagsUsersRequest; res: HashtagsUsersResponse }; + 'i': { req: EmptyRequest; res: IResponse }; + 'i/2fa/done': { req: I2faDoneRequest; res: EmptyResponse }; + 'i/2fa/key-done': { req: I2faKeyDoneRequest; res: EmptyResponse }; + 'i/2fa/password-less': { req: I2faPasswordLessRequest; res: EmptyResponse }; + 'i/2fa/register-key': { req: I2faRegisterKeyRequest; res: EmptyResponse }; + 'i/2fa/register': { req: I2faRegisterRequest; res: EmptyResponse }; + 'i/2fa/update-key': { req: I2faUpdateKeyRequest; res: EmptyResponse }; + 'i/2fa/remove-key': { req: I2faRemoveKeyRequest; res: EmptyResponse }; + 'i/2fa/unregister': { req: I2faUnregisterRequest; res: EmptyResponse }; + 'i/apps': { req: IAppsRequest; res: EmptyResponse }; + 'i/authorized-apps': { req: IAuthorizedAppsRequest; res: EmptyResponse }; + 'i/claim-achievement': { req: IClaimAchievementRequest; res: EmptyResponse }; + 'i/change-password': { req: IChangePasswordRequest; res: EmptyResponse }; + 'i/delete-account': { req: IDeleteAccountRequest; res: EmptyResponse }; + 'i/export-blocking': { req: EmptyRequest; res: EmptyResponse }; + 'i/export-following': { req: IExportFollowingRequest; res: EmptyResponse }; + 'i/export-mute': { req: EmptyRequest; res: EmptyResponse }; + 'i/export-notes': { 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 }; + 'i/favorites': { req: IFavoritesRequest; res: IFavoritesResponse }; + 'i/gallery/likes': { req: IGalleryLikesRequest; res: IGalleryLikesResponse }; + 'i/gallery/posts': { req: IGalleryPostsRequest; res: IGalleryPostsResponse }; + 'i/import-blocking': { req: IImportBlockingRequest; res: EmptyResponse }; + 'i/import-following': { req: IImportFollowingRequest; res: EmptyResponse }; + 'i/import-muting': { req: IImportMutingRequest; res: EmptyResponse }; + 'i/import-user-lists': { req: IImportUserListsRequest; res: EmptyResponse }; + 'i/import-antennas': { req: IImportAntennasRequest; res: EmptyResponse }; + 'i/notifications': { req: INotificationsRequest; res: INotificationsResponse }; + 'i/notifications-grouped': { req: INotificationsGroupedRequest; res: INotificationsGroupedResponse }; + 'i/page-likes': { req: IPageLikesRequest; res: IPageLikesResponse }; + 'i/pages': { req: IPagesRequest; res: IPagesResponse }; + 'i/pin': { req: IPinRequest; res: IPinResponse }; + 'i/read-all-unread-notes': { req: EmptyRequest; res: EmptyResponse }; + 'i/read-announcement': { req: IReadAnnouncementRequest; res: EmptyResponse }; + 'i/regenerate-token': { req: IRegenerateTokenRequest; res: EmptyResponse }; + 'i/registry/get-all': { req: IRegistryGetAllRequest; res: EmptyResponse }; + 'i/registry/get-detail': { req: IRegistryGetDetailRequest; res: EmptyResponse }; + 'i/registry/get': { req: IRegistryGetRequest; res: EmptyResponse }; + 'i/registry/keys-with-type': { req: IRegistryKeysWithTypeRequest; res: EmptyResponse }; + 'i/registry/keys': { req: IRegistryKeysRequest; res: EmptyResponse }; + 'i/registry/remove': { req: IRegistryRemoveRequest; res: EmptyResponse }; + 'i/registry/scopes-with-domain': { req: EmptyRequest; res: EmptyResponse }; + 'i/registry/set': { req: IRegistrySetRequest; res: EmptyResponse }; + 'i/revoke-token': { req: IRevokeTokenRequest; res: EmptyResponse }; + 'i/signin-history': { req: ISigninHistoryRequest; res: ISigninHistoryResponse }; + 'i/unpin': { req: IUnpinRequest; res: IUnpinResponse }; + 'i/update-email': { req: IUpdateEmailRequest; res: EmptyResponse }; + 'i/update': { req: IUpdateRequest; res: IUpdateResponse }; + 'i/move': { req: IMoveRequest; res: EmptyResponse }; + 'i/webhooks/create': { req: IWebhooksCreateRequest; res: EmptyResponse }; + 'i/webhooks/list': { req: EmptyRequest; res: EmptyResponse }; + 'i/webhooks/show': { req: IWebhooksShowRequest; res: EmptyResponse }; + 'i/webhooks/update': { req: IWebhooksUpdateRequest; res: EmptyResponse }; + 'i/webhooks/delete': { req: IWebhooksDeleteRequest; res: EmptyResponse }; + 'invite/create': { req: EmptyRequest; res: InviteCreateResponse }; + 'invite/delete': { req: InviteDeleteRequest; res: EmptyResponse }; + 'invite/list': { req: InviteListRequest; res: InviteListResponse }; + 'invite/limit': { req: EmptyRequest; res: InviteLimitResponse }; + 'meta': { req: MetaRequest; res: MetaResponse }; + 'emojis': { req: EmptyRequest; res: EmojisResponse }; + 'emoji': { req: EmojiRequest; res: EmojiResponse }; + 'miauth/gen-token': { req: MiauthGenTokenRequest; res: MiauthGenTokenResponse }; + 'mute/create': { req: MuteCreateRequest; res: EmptyResponse }; + 'mute/delete': { req: MuteDeleteRequest; res: EmptyResponse }; + 'mute/list': { req: MuteListRequest; res: MuteListResponse }; + 'renote-mute/create': { req: RenoteMuteCreateRequest; res: EmptyResponse }; + 'renote-mute/delete': { req: RenoteMuteDeleteRequest; res: EmptyResponse }; + 'renote-mute/list': { req: RenoteMuteListRequest; res: RenoteMuteListResponse }; + 'my/apps': { req: MyAppsRequest; res: MyAppsResponse }; + 'notes': { req: NotesRequest; res: NotesResponse }; + 'notes/children': { req: NotesChildrenRequest; res: NotesChildrenResponse }; + 'notes/clips': { req: NotesClipsRequest; res: NotesClipsResponse }; + 'notes/conversation': { req: NotesConversationRequest; res: NotesConversationResponse }; + 'notes/create': { req: NotesCreateRequest; res: NotesCreateResponse }; + 'notes/delete': { req: NotesDeleteRequest; res: EmptyResponse }; + 'notes/favorites/create': { req: NotesFavoritesCreateRequest; res: EmptyResponse }; + 'notes/favorites/delete': { req: NotesFavoritesDeleteRequest; res: EmptyResponse }; + 'notes/featured': { req: NotesFeaturedRequest; res: NotesFeaturedResponse }; + 'notes/global-timeline': { req: NotesGlobalTimelineRequest; res: NotesGlobalTimelineResponse }; + 'notes/hybrid-timeline': { req: NotesHybridTimelineRequest; res: NotesHybridTimelineResponse }; + 'notes/local-timeline': { req: NotesLocalTimelineRequest; res: NotesLocalTimelineResponse }; + 'notes/mentions': { req: NotesMentionsRequest; res: NotesMentionsResponse }; + 'notes/polls/recommendation': { req: NotesPollsRecommendationRequest; res: NotesPollsRecommendationResponse }; + 'notes/polls/vote': { req: NotesPollsVoteRequest; res: EmptyResponse }; + 'notes/reactions': { req: NotesReactionsRequest; res: NotesReactionsResponse }; + 'notes/reactions/create': { req: NotesReactionsCreateRequest; res: EmptyResponse }; + 'notes/reactions/delete': { req: NotesReactionsDeleteRequest; res: EmptyResponse }; + 'notes/renotes': { req: NotesRenotesRequest; res: NotesRenotesResponse }; + 'notes/replies': { req: NotesRepliesRequest; res: NotesRepliesResponse }; + 'notes/search-by-tag': { req: NotesSearchByTagRequest; res: NotesSearchByTagResponse }; + 'notes/search': { req: NotesSearchRequest; res: NotesSearchResponse }; + 'notes/show': { req: NotesShowRequest; res: NotesShowResponse }; + 'notes/state': { req: NotesStateRequest; res: NotesStateResponse }; + 'notes/thread-muting/create': { req: NotesThreadMutingCreateRequest; res: EmptyResponse }; + 'notes/thread-muting/delete': { req: NotesThreadMutingDeleteRequest; res: EmptyResponse }; + 'notes/timeline': { req: NotesTimelineRequest; res: NotesTimelineResponse }; + 'notes/translate': { req: NotesTranslateRequest; res: NotesTranslateResponse }; + 'notes/unrenote': { req: NotesUnrenoteRequest; res: EmptyResponse }; + 'notes/user-list-timeline': { req: NotesUserListTimelineRequest; res: NotesUserListTimelineResponse }; + 'notifications/create': { req: NotificationsCreateRequest; res: EmptyResponse }; + 'notifications/mark-all-as-read': { req: EmptyRequest; res: EmptyResponse }; + 'notifications/test-notification': { req: EmptyRequest; res: EmptyResponse }; + 'page-push': { req: PagePushRequest; res: EmptyResponse }; + 'pages/create': { req: PagesCreateRequest; res: PagesCreateResponse }; + 'pages/delete': { req: PagesDeleteRequest; res: EmptyResponse }; + 'pages/featured': { req: EmptyRequest; res: PagesFeaturedResponse }; + 'pages/like': { req: PagesLikeRequest; res: EmptyResponse }; + 'pages/show': { req: PagesShowRequest; res: PagesShowResponse }; + 'pages/unlike': { req: PagesUnlikeRequest; res: EmptyResponse }; + 'pages/update': { req: PagesUpdateRequest; res: EmptyResponse }; + 'flash/create': { req: FlashCreateRequest; res: EmptyResponse }; + 'flash/delete': { req: FlashDeleteRequest; res: EmptyResponse }; + 'flash/featured': { req: EmptyRequest; res: FlashFeaturedResponse }; + 'flash/like': { req: FlashLikeRequest; res: EmptyResponse }; + 'flash/show': { req: FlashShowRequest; res: FlashShowResponse }; + 'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse }; + 'flash/update': { req: FlashUpdateRequest; res: EmptyResponse }; + 'flash/my': { req: FlashMyRequest; res: FlashMyResponse }; + 'flash/my-likes': { req: FlashMyLikesRequest; res: FlashMyLikesResponse }; + 'ping': { req: EmptyRequest; res: PingResponse }; + 'pinned-users': { req: EmptyRequest; res: PinnedUsersResponse }; + 'promo/read': { req: PromoReadRequest; res: EmptyResponse }; + 'roles/list': { req: EmptyRequest; res: RolesListResponse }; + 'roles/show': { req: RolesShowRequest; res: RolesShowResponse }; + 'roles/users': { req: RolesUsersRequest; res: EmptyResponse }; + 'roles/notes': { req: RolesNotesRequest; res: RolesNotesResponse }; + 'request-reset-password': { req: RequestResetPasswordRequest; res: EmptyResponse }; + 'reset-db': { req: EmptyRequest; res: EmptyResponse }; + 'reset-password': { req: ResetPasswordRequest; res: EmptyResponse }; + 'server-info': { req: EmptyRequest; res: EmptyResponse }; + 'stats': { req: EmptyRequest; res: StatsResponse }; + 'sw/show-registration': { req: SwShowRegistrationRequest; res: SwShowRegistrationResponse }; + 'sw/update-registration': { req: SwUpdateRegistrationRequest; res: SwUpdateRegistrationResponse }; + 'sw/register': { req: SwRegisterRequest; res: SwRegisterResponse }; + 'sw/unregister': { req: SwUnregisterRequest; res: EmptyResponse }; + 'test': { req: TestRequest; res: EmptyResponse }; + 'username/available': { req: UsernameAvailableRequest; res: UsernameAvailableResponse }; + 'users': { req: UsersRequest; res: UsersResponse }; + 'users/clips': { req: UsersClipsRequest; res: UsersClipsResponse }; + 'users/followers': { req: UsersFollowersRequest; res: UsersFollowersResponse }; + 'users/following': { req: UsersFollowingRequest; res: UsersFollowingResponse }; + 'users/gallery/posts': { req: UsersGalleryPostsRequest; res: UsersGalleryPostsResponse }; + 'users/get-frequently-replied-users': { req: UsersGetFrequentlyRepliedUsersRequest; res: UsersGetFrequentlyRepliedUsersResponse }; + 'users/featured-notes': { req: UsersFeaturedNotesRequest; res: UsersFeaturedNotesResponse }; + 'users/lists/create': { req: UsersListsCreateRequest; res: UsersListsCreateResponse }; + 'users/lists/delete': { req: UsersListsDeleteRequest; res: EmptyResponse }; + 'users/lists/list': { req: UsersListsListRequest; res: UsersListsListResponse }; + 'users/lists/pull': { req: UsersListsPullRequest; res: EmptyResponse }; + 'users/lists/push': { req: UsersListsPushRequest; res: EmptyResponse }; + 'users/lists/show': { req: UsersListsShowRequest; res: UsersListsShowResponse }; + 'users/lists/favorite': { req: UsersListsFavoriteRequest; res: EmptyResponse }; + 'users/lists/unfavorite': { req: UsersListsUnfavoriteRequest; res: EmptyResponse }; + 'users/lists/update': { req: UsersListsUpdateRequest; res: UsersListsUpdateResponse }; + 'users/lists/create-from-public': { req: UsersListsCreateFromPublicRequest; res: UsersListsCreateFromPublicResponse }; + 'users/lists/update-membership': { req: UsersListsUpdateMembershipRequest; res: EmptyResponse }; + 'users/lists/get-memberships': { req: UsersListsGetMembershipsRequest; res: EmptyResponse }; + 'users/notes': { req: UsersNotesRequest; res: UsersNotesResponse }; + 'users/pages': { req: UsersPagesRequest; res: UsersPagesResponse }; + 'users/flashs': { req: UsersFlashsRequest; res: UsersFlashsResponse }; + 'users/reactions': { req: UsersReactionsRequest; res: UsersReactionsResponse }; + 'users/recommendation': { req: UsersRecommendationRequest; res: UsersRecommendationResponse }; + 'users/relation': { req: UsersRelationRequest; res: UsersRelationResponse }; + 'users/report-abuse': { req: UsersReportAbuseRequest; res: EmptyResponse }; + 'users/search-by-username-and-host': { req: UsersSearchByUsernameAndHostRequest; res: UsersSearchByUsernameAndHostResponse }; + 'users/search': { req: UsersSearchRequest; res: UsersSearchResponse }; + 'users/show': { req: UsersShowRequest; res: UsersShowResponse }; + 'users/achievements': { req: UsersAchievementsRequest; res: EmptyResponse }; + 'users/update-memo': { req: UsersUpdateMemoRequest; res: EmptyResponse }; + 'fetch-rss': { req: FetchRssRequest; res: EmptyResponse }; + 'fetch-external-resources': { req: FetchExternalResourcesRequest; res: EmptyResponse }; + 'retention': { req: EmptyRequest; res: RetentionResponse }; +} diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts new file mode 100644 index 0000000000..4de3c80a7f --- /dev/null +++ b/packages/misskey-js/src/autogen/entities.ts @@ -0,0 +1,512 @@ +/* + * version: 2023.11.0-beta.3 + * generatedAt: 2023-12-08T04:57:48.409Z + */ + +import { operations } from './types.js'; + +export type EmptyRequest = Record<string, unknown> | undefined; +export type EmptyResponse = Record<string, unknown> | undefined; + +export type AdminMetaResponse = operations['admin/meta']['responses']['200']['content']['application/json']; +export type AdminAbuseUserReportsRequest = operations['admin/abuse-user-reports']['requestBody']['content']['application/json']; +export type AdminAbuseUserReportsResponse = operations['admin/abuse-user-reports']['responses']['200']['content']['application/json']; +export type AdminAccountsCreateRequest = operations['admin/accounts/create']['requestBody']['content']['application/json']; +export type AdminAccountsCreateResponse = operations['admin/accounts/create']['responses']['200']['content']['application/json']; +export type AdminAccountsDeleteRequest = operations['admin/accounts/delete']['requestBody']['content']['application/json']; +export type AdminAccountsFindByEmailRequest = operations['admin/accounts/find-by-email']['requestBody']['content']['application/json']; +export type AdminAdCreateRequest = operations['admin/ad/create']['requestBody']['content']['application/json']; +export type AdminAdDeleteRequest = operations['admin/ad/delete']['requestBody']['content']['application/json']; +export type AdminAdListRequest = operations['admin/ad/list']['requestBody']['content']['application/json']; +export type AdminAdUpdateRequest = operations['admin/ad/update']['requestBody']['content']['application/json']; +export type AdminAnnouncementsCreateRequest = operations['admin/announcements/create']['requestBody']['content']['application/json']; +export type AdminAnnouncementsCreateResponse = operations['admin/announcements/create']['responses']['200']['content']['application/json']; +export type AdminAnnouncementsDeleteRequest = operations['admin/announcements/delete']['requestBody']['content']['application/json']; +export type AdminAnnouncementsListRequest = operations['admin/announcements/list']['requestBody']['content']['application/json']; +export type AdminAnnouncementsListResponse = operations['admin/announcements/list']['responses']['200']['content']['application/json']; +export type AdminAnnouncementsUpdateRequest = operations['admin/announcements/update']['requestBody']['content']['application/json']; +export type AdminAvatarDecorationsCreateRequest = operations['admin/avatar-decorations/create']['requestBody']['content']['application/json']; +export type AdminAvatarDecorationsDeleteRequest = operations['admin/avatar-decorations/delete']['requestBody']['content']['application/json']; +export type AdminAvatarDecorationsListRequest = operations['admin/avatar-decorations/list']['requestBody']['content']['application/json']; +export type AdminAvatarDecorationsListResponse = operations['admin/avatar-decorations/list']['responses']['200']['content']['application/json']; +export type AdminAvatarDecorationsUpdateRequest = operations['admin/avatar-decorations/update']['requestBody']['content']['application/json']; +export type AdminDeleteAllFilesOfAUserRequest = operations['admin/delete-all-files-of-a-user']['requestBody']['content']['application/json']; +export type AdminUnsetUserAvatarRequest = operations['admin/unset-user-avatar']['requestBody']['content']['application/json']; +export type AdminUnsetUserBannerRequest = operations['admin/unset-user-banner']['requestBody']['content']['application/json']; +export type AdminDriveFilesRequest = operations['admin/drive/files']['requestBody']['content']['application/json']; +export type AdminDriveFilesResponse = operations['admin/drive/files']['responses']['200']['content']['application/json']; +export type AdminDriveShowFileRequest = operations['admin/drive/show-file']['requestBody']['content']['application/json']; +export type AdminDriveShowFileResponse = operations['admin/drive/show-file']['responses']['200']['content']['application/json']; +export type AdminEmojiAddAliasesBulkRequest = operations['admin/emoji/add-aliases-bulk']['requestBody']['content']['application/json']; +export type AdminEmojiAddRequest = operations['admin/emoji/add']['requestBody']['content']['application/json']; +export type AdminEmojiCopyRequest = operations['admin/emoji/copy']['requestBody']['content']['application/json']; +export type AdminEmojiCopyResponse = operations['admin/emoji/copy']['responses']['200']['content']['application/json']; +export type AdminEmojiDeleteBulkRequest = operations['admin/emoji/delete-bulk']['requestBody']['content']['application/json']; +export type AdminEmojiDeleteRequest = operations['admin/emoji/delete']['requestBody']['content']['application/json']; +export type AdminEmojiImportZipRequest = operations['admin/emoji/import-zip']['requestBody']['content']['application/json']; +export type AdminEmojiListRemoteRequest = operations['admin/emoji/list-remote']['requestBody']['content']['application/json']; +export type AdminEmojiListRemoteResponse = operations['admin/emoji/list-remote']['responses']['200']['content']['application/json']; +export type AdminEmojiListRequest = operations['admin/emoji/list']['requestBody']['content']['application/json']; +export type AdminEmojiListResponse = operations['admin/emoji/list']['responses']['200']['content']['application/json']; +export type AdminEmojiRemoveAliasesBulkRequest = operations['admin/emoji/remove-aliases-bulk']['requestBody']['content']['application/json']; +export type AdminEmojiSetAliasesBulkRequest = operations['admin/emoji/set-aliases-bulk']['requestBody']['content']['application/json']; +export type AdminEmojiSetCategoryBulkRequest = operations['admin/emoji/set-category-bulk']['requestBody']['content']['application/json']; +export type AdminEmojiSetLicenseBulkRequest = operations['admin/emoji/set-license-bulk']['requestBody']['content']['application/json']; +export type AdminEmojiUpdateRequest = operations['admin/emoji/update']['requestBody']['content']['application/json']; +export type AdminFederationDeleteAllFilesRequest = operations['admin/federation/delete-all-files']['requestBody']['content']['application/json']; +export type AdminFederationRefreshRemoteInstanceMetadataRequest = operations['admin/federation/refresh-remote-instance-metadata']['requestBody']['content']['application/json']; +export type AdminFederationRemoveAllFollowingRequest = operations['admin/federation/remove-all-following']['requestBody']['content']['application/json']; +export type AdminFederationUpdateInstanceRequest = operations['admin/federation/update-instance']['requestBody']['content']['application/json']; +export type AdminGetTableStatsResponse = operations['admin/get-table-stats']['responses']['200']['content']['application/json']; +export type AdminGetUserIpsRequest = operations['admin/get-user-ips']['requestBody']['content']['application/json']; +export type AdminInviteCreateRequest = operations['admin/invite/create']['requestBody']['content']['application/json']; +export type AdminInviteCreateResponse = operations['admin/invite/create']['responses']['200']['content']['application/json']; +export type AdminInviteListRequest = operations['admin/invite/list']['requestBody']['content']['application/json']; +export type AdminInviteListResponse = operations['admin/invite/list']['responses']['200']['content']['application/json']; +export type AdminPromoCreateRequest = operations['admin/promo/create']['requestBody']['content']['application/json']; +export type AdminQueueDeliverDelayedResponse = operations['admin/queue/deliver-delayed']['responses']['200']['content']['application/json']; +export type AdminQueueInboxDelayedResponse = operations['admin/queue/inbox-delayed']['responses']['200']['content']['application/json']; +export type AdminQueuePromoteRequest = operations['admin/queue/promote']['requestBody']['content']['application/json']; +export type AdminQueueStatsResponse = operations['admin/queue/stats']['responses']['200']['content']['application/json']; +export type AdminRelaysAddRequest = operations['admin/relays/add']['requestBody']['content']['application/json']; +export type AdminRelaysAddResponse = operations['admin/relays/add']['responses']['200']['content']['application/json']; +export type AdminRelaysListResponse = operations['admin/relays/list']['responses']['200']['content']['application/json']; +export type AdminRelaysRemoveRequest = operations['admin/relays/remove']['requestBody']['content']['application/json']; +export type AdminResetPasswordRequest = operations['admin/reset-password']['requestBody']['content']['application/json']; +export type AdminResetPasswordResponse = operations['admin/reset-password']['responses']['200']['content']['application/json']; +export type AdminResolveAbuseUserReportRequest = operations['admin/resolve-abuse-user-report']['requestBody']['content']['application/json']; +export type AdminSendEmailRequest = operations['admin/send-email']['requestBody']['content']['application/json']; +export type AdminServerInfoResponse = operations['admin/server-info']['responses']['200']['content']['application/json']; +export type AdminShowModerationLogsRequest = operations['admin/show-moderation-logs']['requestBody']['content']['application/json']; +export type AdminShowModerationLogsResponse = operations['admin/show-moderation-logs']['responses']['200']['content']['application/json']; +export type AdminShowUserRequest = operations['admin/show-user']['requestBody']['content']['application/json']; +export type AdminShowUserResponse = operations['admin/show-user']['responses']['200']['content']['application/json']; +export type AdminShowUsersRequest = operations['admin/show-users']['requestBody']['content']['application/json']; +export type AdminShowUsersResponse = operations['admin/show-users']['responses']['200']['content']['application/json']; +export type AdminSuspendUserRequest = operations['admin/suspend-user']['requestBody']['content']['application/json']; +export type AdminUnsuspendUserRequest = operations['admin/unsuspend-user']['requestBody']['content']['application/json']; +export type AdminUpdateMetaRequest = operations['admin/update-meta']['requestBody']['content']['application/json']; +export type AdminDeleteAccountRequest = operations['admin/delete-account']['requestBody']['content']['application/json']; +export type AdminDeleteAccountResponse = operations['admin/delete-account']['responses']['200']['content']['application/json']; +export type AdminUpdateUserNoteRequest = operations['admin/update-user-note']['requestBody']['content']['application/json']; +export type AdminRolesCreateRequest = operations['admin/roles/create']['requestBody']['content']['application/json']; +export type AdminRolesCreateResponse = operations['admin/roles/create']['responses']['200']['content']['application/json']; +export type AdminRolesDeleteRequest = operations['admin/roles/delete']['requestBody']['content']['application/json']; +export type AdminRolesListResponse = operations['admin/roles/list']['responses']['200']['content']['application/json']; +export type AdminRolesShowRequest = operations['admin/roles/show']['requestBody']['content']['application/json']; +export type AdminRolesShowResponse = operations['admin/roles/show']['responses']['200']['content']['application/json']; +export type AdminRolesUpdateRequest = operations['admin/roles/update']['requestBody']['content']['application/json']; +export type AdminRolesAssignRequest = operations['admin/roles/assign']['requestBody']['content']['application/json']; +export type AdminRolesUnassignRequest = operations['admin/roles/unassign']['requestBody']['content']['application/json']; +export type AdminRolesUpdateDefaultPoliciesRequest = operations['admin/roles/update-default-policies']['requestBody']['content']['application/json']; +export type AdminRolesUsersRequest = operations['admin/roles/users']['requestBody']['content']['application/json']; +export type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json']; +export type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json']; +export type AntennasCreateRequest = operations['antennas/create']['requestBody']['content']['application/json']; +export type AntennasCreateResponse = operations['antennas/create']['responses']['200']['content']['application/json']; +export type AntennasDeleteRequest = operations['antennas/delete']['requestBody']['content']['application/json']; +export type AntennasListResponse = operations['antennas/list']['responses']['200']['content']['application/json']; +export type AntennasNotesRequest = operations['antennas/notes']['requestBody']['content']['application/json']; +export type AntennasNotesResponse = operations['antennas/notes']['responses']['200']['content']['application/json']; +export type AntennasShowRequest = operations['antennas/show']['requestBody']['content']['application/json']; +export type AntennasShowResponse = operations['antennas/show']['responses']['200']['content']['application/json']; +export type AntennasUpdateRequest = operations['antennas/update']['requestBody']['content']['application/json']; +export type AntennasUpdateResponse = operations['antennas/update']['responses']['200']['content']['application/json']; +export type ApGetRequest = operations['ap/get']['requestBody']['content']['application/json']; +export type ApGetResponse = operations['ap/get']['responses']['200']['content']['application/json']; +export type ApShowRequest = operations['ap/show']['requestBody']['content']['application/json']; +export type ApShowResponse = operations['ap/show']['responses']['200']['content']['application/json']; +export type AppCreateRequest = operations['app/create']['requestBody']['content']['application/json']; +export type AppCreateResponse = operations['app/create']['responses']['200']['content']['application/json']; +export type AppShowRequest = operations['app/show']['requestBody']['content']['application/json']; +export type AppShowResponse = operations['app/show']['responses']['200']['content']['application/json']; +export type AuthAcceptRequest = operations['auth/accept']['requestBody']['content']['application/json']; +export type AuthSessionGenerateRequest = operations['auth/session/generate']['requestBody']['content']['application/json']; +export type AuthSessionGenerateResponse = operations['auth/session/generate']['responses']['200']['content']['application/json']; +export type AuthSessionShowRequest = operations['auth/session/show']['requestBody']['content']['application/json']; +export type AuthSessionShowResponse = operations['auth/session/show']['responses']['200']['content']['application/json']; +export type AuthSessionUserkeyRequest = operations['auth/session/userkey']['requestBody']['content']['application/json']; +export type AuthSessionUserkeyResponse = operations['auth/session/userkey']['responses']['200']['content']['application/json']; +export type BlockingCreateRequest = operations['blocking/create']['requestBody']['content']['application/json']; +export type BlockingCreateResponse = operations['blocking/create']['responses']['200']['content']['application/json']; +export type BlockingDeleteRequest = operations['blocking/delete']['requestBody']['content']['application/json']; +export type BlockingDeleteResponse = operations['blocking/delete']['responses']['200']['content']['application/json']; +export type BlockingListRequest = operations['blocking/list']['requestBody']['content']['application/json']; +export type BlockingListResponse = operations['blocking/list']['responses']['200']['content']['application/json']; +export type ChannelsCreateRequest = operations['channels/create']['requestBody']['content']['application/json']; +export type ChannelsCreateResponse = operations['channels/create']['responses']['200']['content']['application/json']; +export type ChannelsFeaturedResponse = operations['channels/featured']['responses']['200']['content']['application/json']; +export type ChannelsFollowRequest = operations['channels/follow']['requestBody']['content']['application/json']; +export type ChannelsFollowedRequest = operations['channels/followed']['requestBody']['content']['application/json']; +export type ChannelsFollowedResponse = operations['channels/followed']['responses']['200']['content']['application/json']; +export type ChannelsOwnedRequest = operations['channels/owned']['requestBody']['content']['application/json']; +export type ChannelsOwnedResponse = operations['channels/owned']['responses']['200']['content']['application/json']; +export type ChannelsShowRequest = operations['channels/show']['requestBody']['content']['application/json']; +export type ChannelsShowResponse = operations['channels/show']['responses']['200']['content']['application/json']; +export type ChannelsTimelineRequest = operations['channels/timeline']['requestBody']['content']['application/json']; +export type ChannelsTimelineResponse = operations['channels/timeline']['responses']['200']['content']['application/json']; +export type ChannelsUnfollowRequest = operations['channels/unfollow']['requestBody']['content']['application/json']; +export type ChannelsUpdateRequest = operations['channels/update']['requestBody']['content']['application/json']; +export type ChannelsUpdateResponse = operations['channels/update']['responses']['200']['content']['application/json']; +export type ChannelsFavoriteRequest = operations['channels/favorite']['requestBody']['content']['application/json']; +export type ChannelsUnfavoriteRequest = operations['channels/unfavorite']['requestBody']['content']['application/json']; +export type ChannelsMyFavoritesResponse = operations['channels/my-favorites']['responses']['200']['content']['application/json']; +export type ChannelsSearchRequest = operations['channels/search']['requestBody']['content']['application/json']; +export type ChannelsSearchResponse = operations['channels/search']['responses']['200']['content']['application/json']; +export type ChartsActiveUsersRequest = operations['charts/active-users']['requestBody']['content']['application/json']; +export type ChartsActiveUsersResponse = operations['charts/active-users']['responses']['200']['content']['application/json']; +export type ChartsApRequestRequest = operations['charts/ap-request']['requestBody']['content']['application/json']; +export type ChartsApRequestResponse = operations['charts/ap-request']['responses']['200']['content']['application/json']; +export type ChartsDriveRequest = operations['charts/drive']['requestBody']['content']['application/json']; +export type ChartsDriveResponse = operations['charts/drive']['responses']['200']['content']['application/json']; +export type ChartsFederationRequest = operations['charts/federation']['requestBody']['content']['application/json']; +export type ChartsFederationResponse = operations['charts/federation']['responses']['200']['content']['application/json']; +export type ChartsInstanceRequest = operations['charts/instance']['requestBody']['content']['application/json']; +export type ChartsInstanceResponse = operations['charts/instance']['responses']['200']['content']['application/json']; +export type ChartsNotesRequest = operations['charts/notes']['requestBody']['content']['application/json']; +export type ChartsNotesResponse = operations['charts/notes']['responses']['200']['content']['application/json']; +export type ChartsUserDriveRequest = operations['charts/user/drive']['requestBody']['content']['application/json']; +export type ChartsUserDriveResponse = operations['charts/user/drive']['responses']['200']['content']['application/json']; +export type ChartsUserFollowingRequest = operations['charts/user/following']['requestBody']['content']['application/json']; +export type ChartsUserFollowingResponse = operations['charts/user/following']['responses']['200']['content']['application/json']; +export type ChartsUserNotesRequest = operations['charts/user/notes']['requestBody']['content']['application/json']; +export type ChartsUserNotesResponse = operations['charts/user/notes']['responses']['200']['content']['application/json']; +export type ChartsUserPvRequest = operations['charts/user/pv']['requestBody']['content']['application/json']; +export type ChartsUserPvResponse = operations['charts/user/pv']['responses']['200']['content']['application/json']; +export type ChartsUserReactionsRequest = operations['charts/user/reactions']['requestBody']['content']['application/json']; +export type ChartsUserReactionsResponse = operations['charts/user/reactions']['responses']['200']['content']['application/json']; +export type ChartsUsersRequest = operations['charts/users']['requestBody']['content']['application/json']; +export type ChartsUsersResponse = operations['charts/users']['responses']['200']['content']['application/json']; +export type ClipsAddNoteRequest = operations['clips/add-note']['requestBody']['content']['application/json']; +export type ClipsRemoveNoteRequest = operations['clips/remove-note']['requestBody']['content']['application/json']; +export type ClipsCreateRequest = operations['clips/create']['requestBody']['content']['application/json']; +export type ClipsCreateResponse = operations['clips/create']['responses']['200']['content']['application/json']; +export type ClipsDeleteRequest = operations['clips/delete']['requestBody']['content']['application/json']; +export type ClipsListResponse = operations['clips/list']['responses']['200']['content']['application/json']; +export type ClipsNotesRequest = operations['clips/notes']['requestBody']['content']['application/json']; +export type ClipsNotesResponse = operations['clips/notes']['responses']['200']['content']['application/json']; +export type ClipsShowRequest = operations['clips/show']['requestBody']['content']['application/json']; +export type ClipsShowResponse = operations['clips/show']['responses']['200']['content']['application/json']; +export type ClipsUpdateRequest = operations['clips/update']['requestBody']['content']['application/json']; +export type ClipsUpdateResponse = operations['clips/update']['responses']['200']['content']['application/json']; +export type ClipsFavoriteRequest = operations['clips/favorite']['requestBody']['content']['application/json']; +export type ClipsUnfavoriteRequest = operations['clips/unfavorite']['requestBody']['content']['application/json']; +export type ClipsMyFavoritesResponse = operations['clips/my-favorites']['responses']['200']['content']['application/json']; +export type DriveResponse = operations['drive']['responses']['200']['content']['application/json']; +export type DriveFilesRequest = operations['drive/files']['requestBody']['content']['application/json']; +export type DriveFilesResponse = operations['drive/files']['responses']['200']['content']['application/json']; +export type DriveFilesAttachedNotesRequest = operations['drive/files/attached-notes']['requestBody']['content']['application/json']; +export type DriveFilesAttachedNotesResponse = operations['drive/files/attached-notes']['responses']['200']['content']['application/json']; +export type DriveFilesCheckExistenceRequest = operations['drive/files/check-existence']['requestBody']['content']['application/json']; +export type DriveFilesCheckExistenceResponse = operations['drive/files/check-existence']['responses']['200']['content']['application/json']; +export type DriveFilesCreateRequest = operations['drive/files/create']['requestBody']['content']['multipart/form-data']; +export type DriveFilesCreateResponse = operations['drive/files/create']['responses']['200']['content']['application/json']; +export type DriveFilesDeleteRequest = operations['drive/files/delete']['requestBody']['content']['application/json']; +export type DriveFilesFindByHashRequest = operations['drive/files/find-by-hash']['requestBody']['content']['application/json']; +export type DriveFilesFindByHashResponse = operations['drive/files/find-by-hash']['responses']['200']['content']['application/json']; +export type DriveFilesFindRequest = operations['drive/files/find']['requestBody']['content']['application/json']; +export type DriveFilesFindResponse = operations['drive/files/find']['responses']['200']['content']['application/json']; +export type DriveFilesShowRequest = operations['drive/files/show']['requestBody']['content']['application/json']; +export type DriveFilesShowResponse = operations['drive/files/show']['responses']['200']['content']['application/json']; +export type DriveFilesUpdateRequest = operations['drive/files/update']['requestBody']['content']['application/json']; +export type DriveFilesUpdateResponse = operations['drive/files/update']['responses']['200']['content']['application/json']; +export type DriveFilesUploadFromUrlRequest = operations['drive/files/upload-from-url']['requestBody']['content']['application/json']; +export type DriveFoldersRequest = operations['drive/folders']['requestBody']['content']['application/json']; +export type DriveFoldersResponse = operations['drive/folders']['responses']['200']['content']['application/json']; +export type DriveFoldersCreateRequest = operations['drive/folders/create']['requestBody']['content']['application/json']; +export type DriveFoldersCreateResponse = operations['drive/folders/create']['responses']['200']['content']['application/json']; +export type DriveFoldersDeleteRequest = operations['drive/folders/delete']['requestBody']['content']['application/json']; +export type DriveFoldersFindRequest = operations['drive/folders/find']['requestBody']['content']['application/json']; +export type DriveFoldersFindResponse = operations['drive/folders/find']['responses']['200']['content']['application/json']; +export type DriveFoldersShowRequest = operations['drive/folders/show']['requestBody']['content']['application/json']; +export type DriveFoldersShowResponse = operations['drive/folders/show']['responses']['200']['content']['application/json']; +export type DriveFoldersUpdateRequest = operations['drive/folders/update']['requestBody']['content']['application/json']; +export type DriveFoldersUpdateResponse = operations['drive/folders/update']['responses']['200']['content']['application/json']; +export type DriveStreamRequest = operations['drive/stream']['requestBody']['content']['application/json']; +export type DriveStreamResponse = operations['drive/stream']['responses']['200']['content']['application/json']; +export type EmailAddressAvailableRequest = operations['email-address/available']['requestBody']['content']['application/json']; +export type EmailAddressAvailableResponse = operations['email-address/available']['responses']['200']['content']['application/json']; +export type EndpointRequest = operations['endpoint']['requestBody']['content']['application/json']; +export type EndpointsResponse = operations['endpoints']['responses']['200']['content']['application/json']; +export type FederationFollowersRequest = operations['federation/followers']['requestBody']['content']['application/json']; +export type FederationFollowersResponse = operations['federation/followers']['responses']['200']['content']['application/json']; +export type FederationFollowingRequest = operations['federation/following']['requestBody']['content']['application/json']; +export type FederationFollowingResponse = operations['federation/following']['responses']['200']['content']['application/json']; +export type FederationInstancesRequest = operations['federation/instances']['requestBody']['content']['application/json']; +export type FederationInstancesResponse = operations['federation/instances']['responses']['200']['content']['application/json']; +export type FederationShowInstanceRequest = operations['federation/show-instance']['requestBody']['content']['application/json']; +export type FederationShowInstanceResponse = operations['federation/show-instance']['responses']['200']['content']['application/json']; +export type FederationUpdateRemoteUserRequest = operations['federation/update-remote-user']['requestBody']['content']['application/json']; +export type FederationUsersRequest = operations['federation/users']['requestBody']['content']['application/json']; +export type FederationUsersResponse = operations['federation/users']['responses']['200']['content']['application/json']; +export type FederationStatsRequest = operations['federation/stats']['requestBody']['content']['application/json']; +export type FollowingCreateRequest = operations['following/create']['requestBody']['content']['application/json']; +export type FollowingCreateResponse = operations['following/create']['responses']['200']['content']['application/json']; +export type FollowingDeleteRequest = operations['following/delete']['requestBody']['content']['application/json']; +export type FollowingDeleteResponse = operations['following/delete']['responses']['200']['content']['application/json']; +export type FollowingUpdateRequest = operations['following/update']['requestBody']['content']['application/json']; +export type FollowingUpdateResponse = operations['following/update']['responses']['200']['content']['application/json']; +export type FollowingUpdateAllRequest = operations['following/update-all']['requestBody']['content']['application/json']; +export type FollowingInvalidateRequest = operations['following/invalidate']['requestBody']['content']['application/json']; +export type FollowingInvalidateResponse = operations['following/invalidate']['responses']['200']['content']['application/json']; +export type FollowingRequestsAcceptRequest = operations['following/requests/accept']['requestBody']['content']['application/json']; +export type FollowingRequestsCancelRequest = operations['following/requests/cancel']['requestBody']['content']['application/json']; +export type FollowingRequestsCancelResponse = operations['following/requests/cancel']['responses']['200']['content']['application/json']; +export type FollowingRequestsListRequest = operations['following/requests/list']['requestBody']['content']['application/json']; +export type FollowingRequestsListResponse = operations['following/requests/list']['responses']['200']['content']['application/json']; +export type FollowingRequestsRejectRequest = operations['following/requests/reject']['requestBody']['content']['application/json']; +export type GalleryFeaturedRequest = operations['gallery/featured']['requestBody']['content']['application/json']; +export type GalleryFeaturedResponse = operations['gallery/featured']['responses']['200']['content']['application/json']; +export type GalleryPopularResponse = operations['gallery/popular']['responses']['200']['content']['application/json']; +export type GalleryPostsRequest = operations['gallery/posts']['requestBody']['content']['application/json']; +export type GalleryPostsResponse = operations['gallery/posts']['responses']['200']['content']['application/json']; +export type GalleryPostsCreateRequest = operations['gallery/posts/create']['requestBody']['content']['application/json']; +export type GalleryPostsCreateResponse = operations['gallery/posts/create']['responses']['200']['content']['application/json']; +export type GalleryPostsDeleteRequest = operations['gallery/posts/delete']['requestBody']['content']['application/json']; +export type GalleryPostsLikeRequest = operations['gallery/posts/like']['requestBody']['content']['application/json']; +export type GalleryPostsShowRequest = operations['gallery/posts/show']['requestBody']['content']['application/json']; +export type GalleryPostsShowResponse = operations['gallery/posts/show']['responses']['200']['content']['application/json']; +export type GalleryPostsUnlikeRequest = operations['gallery/posts/unlike']['requestBody']['content']['application/json']; +export type GalleryPostsUpdateRequest = operations['gallery/posts/update']['requestBody']['content']['application/json']; +export type GalleryPostsUpdateResponse = operations['gallery/posts/update']['responses']['200']['content']['application/json']; +export type GetAvatarDecorationsResponse = operations['get-avatar-decorations']['responses']['200']['content']['application/json']; +export type HashtagsListRequest = operations['hashtags/list']['requestBody']['content']['application/json']; +export type HashtagsListResponse = operations['hashtags/list']['responses']['200']['content']['application/json']; +export type HashtagsSearchRequest = operations['hashtags/search']['requestBody']['content']['application/json']; +export type HashtagsSearchResponse = operations['hashtags/search']['responses']['200']['content']['application/json']; +export type HashtagsShowRequest = operations['hashtags/show']['requestBody']['content']['application/json']; +export type HashtagsShowResponse = operations['hashtags/show']['responses']['200']['content']['application/json']; +export type HashtagsTrendResponse = operations['hashtags/trend']['responses']['200']['content']['application/json']; +export type HashtagsUsersRequest = operations['hashtags/users']['requestBody']['content']['application/json']; +export type HashtagsUsersResponse = operations['hashtags/users']['responses']['200']['content']['application/json']; +export type IResponse = operations['i']['responses']['200']['content']['application/json']; +export type I2faDoneRequest = operations['i/2fa/done']['requestBody']['content']['application/json']; +export type I2faKeyDoneRequest = operations['i/2fa/key-done']['requestBody']['content']['application/json']; +export type I2faPasswordLessRequest = operations['i/2fa/password-less']['requestBody']['content']['application/json']; +export type I2faRegisterKeyRequest = operations['i/2fa/register-key']['requestBody']['content']['application/json']; +export type I2faRegisterRequest = operations['i/2fa/register']['requestBody']['content']['application/json']; +export type I2faUpdateKeyRequest = operations['i/2fa/update-key']['requestBody']['content']['application/json']; +export type I2faRemoveKeyRequest = operations['i/2fa/remove-key']['requestBody']['content']['application/json']; +export type I2faUnregisterRequest = operations['i/2fa/unregister']['requestBody']['content']['application/json']; +export type IAppsRequest = operations['i/apps']['requestBody']['content']['application/json']; +export type IAuthorizedAppsRequest = operations['i/authorized-apps']['requestBody']['content']['application/json']; +export type IClaimAchievementRequest = operations['i/claim-achievement']['requestBody']['content']['application/json']; +export type IChangePasswordRequest = operations['i/change-password']['requestBody']['content']['application/json']; +export type IDeleteAccountRequest = operations['i/delete-account']['requestBody']['content']['application/json']; +export type IExportFollowingRequest = operations['i/export-following']['requestBody']['content']['application/json']; +export type IFavoritesRequest = operations['i/favorites']['requestBody']['content']['application/json']; +export type IFavoritesResponse = operations['i/favorites']['responses']['200']['content']['application/json']; +export type IGalleryLikesRequest = operations['i/gallery/likes']['requestBody']['content']['application/json']; +export type IGalleryLikesResponse = operations['i/gallery/likes']['responses']['200']['content']['application/json']; +export type IGalleryPostsRequest = operations['i/gallery/posts']['requestBody']['content']['application/json']; +export type IGalleryPostsResponse = operations['i/gallery/posts']['responses']['200']['content']['application/json']; +export type IImportBlockingRequest = operations['i/import-blocking']['requestBody']['content']['application/json']; +export type IImportFollowingRequest = operations['i/import-following']['requestBody']['content']['application/json']; +export type IImportMutingRequest = operations['i/import-muting']['requestBody']['content']['application/json']; +export type IImportUserListsRequest = operations['i/import-user-lists']['requestBody']['content']['application/json']; +export type IImportAntennasRequest = operations['i/import-antennas']['requestBody']['content']['application/json']; +export type INotificationsRequest = operations['i/notifications']['requestBody']['content']['application/json']; +export type INotificationsResponse = operations['i/notifications']['responses']['200']['content']['application/json']; +export type INotificationsGroupedRequest = operations['i/notifications-grouped']['requestBody']['content']['application/json']; +export type INotificationsGroupedResponse = operations['i/notifications-grouped']['responses']['200']['content']['application/json']; +export type IPageLikesRequest = operations['i/page-likes']['requestBody']['content']['application/json']; +export type IPageLikesResponse = operations['i/page-likes']['responses']['200']['content']['application/json']; +export type IPagesRequest = operations['i/pages']['requestBody']['content']['application/json']; +export type IPagesResponse = operations['i/pages']['responses']['200']['content']['application/json']; +export type IPinRequest = operations['i/pin']['requestBody']['content']['application/json']; +export type IPinResponse = operations['i/pin']['responses']['200']['content']['application/json']; +export type IReadAnnouncementRequest = operations['i/read-announcement']['requestBody']['content']['application/json']; +export type IRegenerateTokenRequest = operations['i/regenerate-token']['requestBody']['content']['application/json']; +export type IRegistryGetAllRequest = operations['i/registry/get-all']['requestBody']['content']['application/json']; +export type IRegistryGetDetailRequest = operations['i/registry/get-detail']['requestBody']['content']['application/json']; +export type IRegistryGetRequest = operations['i/registry/get']['requestBody']['content']['application/json']; +export type IRegistryKeysWithTypeRequest = operations['i/registry/keys-with-type']['requestBody']['content']['application/json']; +export type IRegistryKeysRequest = operations['i/registry/keys']['requestBody']['content']['application/json']; +export type IRegistryRemoveRequest = operations['i/registry/remove']['requestBody']['content']['application/json']; +export type IRegistrySetRequest = operations['i/registry/set']['requestBody']['content']['application/json']; +export type IRevokeTokenRequest = operations['i/revoke-token']['requestBody']['content']['application/json']; +export type ISigninHistoryRequest = operations['i/signin-history']['requestBody']['content']['application/json']; +export type ISigninHistoryResponse = operations['i/signin-history']['responses']['200']['content']['application/json']; +export type IUnpinRequest = operations['i/unpin']['requestBody']['content']['application/json']; +export type IUnpinResponse = operations['i/unpin']['responses']['200']['content']['application/json']; +export type IUpdateEmailRequest = operations['i/update-email']['requestBody']['content']['application/json']; +export type IUpdateRequest = operations['i/update']['requestBody']['content']['application/json']; +export type IUpdateResponse = operations['i/update']['responses']['200']['content']['application/json']; +export type IMoveRequest = operations['i/move']['requestBody']['content']['application/json']; +export type IWebhooksCreateRequest = operations['i/webhooks/create']['requestBody']['content']['application/json']; +export type IWebhooksShowRequest = operations['i/webhooks/show']['requestBody']['content']['application/json']; +export type IWebhooksUpdateRequest = operations['i/webhooks/update']['requestBody']['content']['application/json']; +export type IWebhooksDeleteRequest = operations['i/webhooks/delete']['requestBody']['content']['application/json']; +export type InviteCreateResponse = operations['invite/create']['responses']['200']['content']['application/json']; +export type InviteDeleteRequest = operations['invite/delete']['requestBody']['content']['application/json']; +export type InviteListRequest = operations['invite/list']['requestBody']['content']['application/json']; +export type InviteListResponse = operations['invite/list']['responses']['200']['content']['application/json']; +export type InviteLimitResponse = operations['invite/limit']['responses']['200']['content']['application/json']; +export type MetaRequest = operations['meta']['requestBody']['content']['application/json']; +export type MetaResponse = operations['meta']['responses']['200']['content']['application/json']; +export type EmojisResponse = operations['emojis']['responses']['200']['content']['application/json']; +export type EmojiRequest = operations['emoji']['requestBody']['content']['application/json']; +export type EmojiResponse = operations['emoji']['responses']['200']['content']['application/json']; +export type MiauthGenTokenRequest = operations['miauth/gen-token']['requestBody']['content']['application/json']; +export type MiauthGenTokenResponse = operations['miauth/gen-token']['responses']['200']['content']['application/json']; +export type MuteCreateRequest = operations['mute/create']['requestBody']['content']['application/json']; +export type MuteDeleteRequest = operations['mute/delete']['requestBody']['content']['application/json']; +export type MuteListRequest = operations['mute/list']['requestBody']['content']['application/json']; +export type MuteListResponse = operations['mute/list']['responses']['200']['content']['application/json']; +export type RenoteMuteCreateRequest = operations['renote-mute/create']['requestBody']['content']['application/json']; +export type RenoteMuteDeleteRequest = operations['renote-mute/delete']['requestBody']['content']['application/json']; +export type RenoteMuteListRequest = operations['renote-mute/list']['requestBody']['content']['application/json']; +export type RenoteMuteListResponse = operations['renote-mute/list']['responses']['200']['content']['application/json']; +export type MyAppsRequest = operations['my/apps']['requestBody']['content']['application/json']; +export type MyAppsResponse = operations['my/apps']['responses']['200']['content']['application/json']; +export type NotesRequest = operations['notes']['requestBody']['content']['application/json']; +export type NotesResponse = operations['notes']['responses']['200']['content']['application/json']; +export type NotesChildrenRequest = operations['notes/children']['requestBody']['content']['application/json']; +export type NotesChildrenResponse = operations['notes/children']['responses']['200']['content']['application/json']; +export type NotesClipsRequest = operations['notes/clips']['requestBody']['content']['application/json']; +export type NotesClipsResponse = operations['notes/clips']['responses']['200']['content']['application/json']; +export type NotesConversationRequest = operations['notes/conversation']['requestBody']['content']['application/json']; +export type NotesConversationResponse = operations['notes/conversation']['responses']['200']['content']['application/json']; +export type NotesCreateRequest = operations['notes/create']['requestBody']['content']['application/json']; +export type NotesCreateResponse = operations['notes/create']['responses']['200']['content']['application/json']; +export type NotesDeleteRequest = operations['notes/delete']['requestBody']['content']['application/json']; +export type NotesFavoritesCreateRequest = operations['notes/favorites/create']['requestBody']['content']['application/json']; +export type NotesFavoritesDeleteRequest = operations['notes/favorites/delete']['requestBody']['content']['application/json']; +export type NotesFeaturedRequest = operations['notes/featured']['requestBody']['content']['application/json']; +export type NotesFeaturedResponse = operations['notes/featured']['responses']['200']['content']['application/json']; +export type NotesGlobalTimelineRequest = operations['notes/global-timeline']['requestBody']['content']['application/json']; +export type NotesGlobalTimelineResponse = operations['notes/global-timeline']['responses']['200']['content']['application/json']; +export type NotesHybridTimelineRequest = operations['notes/hybrid-timeline']['requestBody']['content']['application/json']; +export type NotesHybridTimelineResponse = operations['notes/hybrid-timeline']['responses']['200']['content']['application/json']; +export type NotesLocalTimelineRequest = operations['notes/local-timeline']['requestBody']['content']['application/json']; +export type NotesLocalTimelineResponse = operations['notes/local-timeline']['responses']['200']['content']['application/json']; +export type NotesMentionsRequest = operations['notes/mentions']['requestBody']['content']['application/json']; +export type NotesMentionsResponse = operations['notes/mentions']['responses']['200']['content']['application/json']; +export type NotesPollsRecommendationRequest = operations['notes/polls/recommendation']['requestBody']['content']['application/json']; +export type NotesPollsRecommendationResponse = operations['notes/polls/recommendation']['responses']['200']['content']['application/json']; +export type NotesPollsVoteRequest = operations['notes/polls/vote']['requestBody']['content']['application/json']; +export type NotesReactionsRequest = operations['notes/reactions']['requestBody']['content']['application/json']; +export type NotesReactionsResponse = operations['notes/reactions']['responses']['200']['content']['application/json']; +export type NotesReactionsCreateRequest = operations['notes/reactions/create']['requestBody']['content']['application/json']; +export type NotesReactionsDeleteRequest = operations['notes/reactions/delete']['requestBody']['content']['application/json']; +export type NotesRenotesRequest = operations['notes/renotes']['requestBody']['content']['application/json']; +export type NotesRenotesResponse = operations['notes/renotes']['responses']['200']['content']['application/json']; +export type NotesRepliesRequest = operations['notes/replies']['requestBody']['content']['application/json']; +export type NotesRepliesResponse = operations['notes/replies']['responses']['200']['content']['application/json']; +export type NotesSearchByTagRequest = operations['notes/search-by-tag']['requestBody']['content']['application/json']; +export type NotesSearchByTagResponse = operations['notes/search-by-tag']['responses']['200']['content']['application/json']; +export type NotesSearchRequest = operations['notes/search']['requestBody']['content']['application/json']; +export type NotesSearchResponse = operations['notes/search']['responses']['200']['content']['application/json']; +export type NotesShowRequest = operations['notes/show']['requestBody']['content']['application/json']; +export type NotesShowResponse = operations['notes/show']['responses']['200']['content']['application/json']; +export type NotesStateRequest = operations['notes/state']['requestBody']['content']['application/json']; +export type NotesStateResponse = operations['notes/state']['responses']['200']['content']['application/json']; +export type NotesThreadMutingCreateRequest = operations['notes/thread-muting/create']['requestBody']['content']['application/json']; +export type NotesThreadMutingDeleteRequest = operations['notes/thread-muting/delete']['requestBody']['content']['application/json']; +export type NotesTimelineRequest = operations['notes/timeline']['requestBody']['content']['application/json']; +export type NotesTimelineResponse = operations['notes/timeline']['responses']['200']['content']['application/json']; +export type NotesTranslateRequest = operations['notes/translate']['requestBody']['content']['application/json']; +export type NotesTranslateResponse = operations['notes/translate']['responses']['200']['content']['application/json']; +export type NotesUnrenoteRequest = operations['notes/unrenote']['requestBody']['content']['application/json']; +export type NotesUserListTimelineRequest = operations['notes/user-list-timeline']['requestBody']['content']['application/json']; +export type NotesUserListTimelineResponse = operations['notes/user-list-timeline']['responses']['200']['content']['application/json']; +export type NotificationsCreateRequest = operations['notifications/create']['requestBody']['content']['application/json']; +export type PagePushRequest = operations['page-push']['requestBody']['content']['application/json']; +export type PagesCreateRequest = operations['pages/create']['requestBody']['content']['application/json']; +export type PagesCreateResponse = operations['pages/create']['responses']['200']['content']['application/json']; +export type PagesDeleteRequest = operations['pages/delete']['requestBody']['content']['application/json']; +export type PagesFeaturedResponse = operations['pages/featured']['responses']['200']['content']['application/json']; +export type PagesLikeRequest = operations['pages/like']['requestBody']['content']['application/json']; +export type PagesShowRequest = operations['pages/show']['requestBody']['content']['application/json']; +export type PagesShowResponse = operations['pages/show']['responses']['200']['content']['application/json']; +export type PagesUnlikeRequest = operations['pages/unlike']['requestBody']['content']['application/json']; +export type PagesUpdateRequest = operations['pages/update']['requestBody']['content']['application/json']; +export type FlashCreateRequest = operations['flash/create']['requestBody']['content']['application/json']; +export type FlashDeleteRequest = operations['flash/delete']['requestBody']['content']['application/json']; +export type FlashFeaturedResponse = operations['flash/featured']['responses']['200']['content']['application/json']; +export type FlashLikeRequest = operations['flash/like']['requestBody']['content']['application/json']; +export type FlashShowRequest = operations['flash/show']['requestBody']['content']['application/json']; +export type FlashShowResponse = operations['flash/show']['responses']['200']['content']['application/json']; +export type FlashUnlikeRequest = operations['flash/unlike']['requestBody']['content']['application/json']; +export type FlashUpdateRequest = operations['flash/update']['requestBody']['content']['application/json']; +export type FlashMyRequest = operations['flash/my']['requestBody']['content']['application/json']; +export type FlashMyResponse = operations['flash/my']['responses']['200']['content']['application/json']; +export type FlashMyLikesRequest = operations['flash/my-likes']['requestBody']['content']['application/json']; +export type FlashMyLikesResponse = operations['flash/my-likes']['responses']['200']['content']['application/json']; +export type PingResponse = operations['ping']['responses']['200']['content']['application/json']; +export type PinnedUsersResponse = operations['pinned-users']['responses']['200']['content']['application/json']; +export type PromoReadRequest = operations['promo/read']['requestBody']['content']['application/json']; +export type RolesListResponse = operations['roles/list']['responses']['200']['content']['application/json']; +export type RolesShowRequest = operations['roles/show']['requestBody']['content']['application/json']; +export type RolesShowResponse = operations['roles/show']['responses']['200']['content']['application/json']; +export type RolesUsersRequest = operations['roles/users']['requestBody']['content']['application/json']; +export type RolesNotesRequest = operations['roles/notes']['requestBody']['content']['application/json']; +export type RolesNotesResponse = operations['roles/notes']['responses']['200']['content']['application/json']; +export type RequestResetPasswordRequest = operations['request-reset-password']['requestBody']['content']['application/json']; +export type ResetPasswordRequest = operations['reset-password']['requestBody']['content']['application/json']; +export type StatsResponse = operations['stats']['responses']['200']['content']['application/json']; +export type SwShowRegistrationRequest = operations['sw/show-registration']['requestBody']['content']['application/json']; +export type SwShowRegistrationResponse = operations['sw/show-registration']['responses']['200']['content']['application/json']; +export type SwUpdateRegistrationRequest = operations['sw/update-registration']['requestBody']['content']['application/json']; +export type SwUpdateRegistrationResponse = operations['sw/update-registration']['responses']['200']['content']['application/json']; +export type SwRegisterRequest = operations['sw/register']['requestBody']['content']['application/json']; +export type SwRegisterResponse = operations['sw/register']['responses']['200']['content']['application/json']; +export type SwUnregisterRequest = operations['sw/unregister']['requestBody']['content']['application/json']; +export type TestRequest = operations['test']['requestBody']['content']['application/json']; +export type UsernameAvailableRequest = operations['username/available']['requestBody']['content']['application/json']; +export type UsernameAvailableResponse = operations['username/available']['responses']['200']['content']['application/json']; +export type UsersRequest = operations['users']['requestBody']['content']['application/json']; +export type UsersResponse = operations['users']['responses']['200']['content']['application/json']; +export type UsersClipsRequest = operations['users/clips']['requestBody']['content']['application/json']; +export type UsersClipsResponse = operations['users/clips']['responses']['200']['content']['application/json']; +export type UsersFollowersRequest = operations['users/followers']['requestBody']['content']['application/json']; +export type UsersFollowersResponse = operations['users/followers']['responses']['200']['content']['application/json']; +export type UsersFollowingRequest = operations['users/following']['requestBody']['content']['application/json']; +export type UsersFollowingResponse = operations['users/following']['responses']['200']['content']['application/json']; +export type UsersGalleryPostsRequest = operations['users/gallery/posts']['requestBody']['content']['application/json']; +export type UsersGalleryPostsResponse = operations['users/gallery/posts']['responses']['200']['content']['application/json']; +export type UsersGetFrequentlyRepliedUsersRequest = operations['users/get-frequently-replied-users']['requestBody']['content']['application/json']; +export type UsersGetFrequentlyRepliedUsersResponse = operations['users/get-frequently-replied-users']['responses']['200']['content']['application/json']; +export type UsersFeaturedNotesRequest = operations['users/featured-notes']['requestBody']['content']['application/json']; +export type UsersFeaturedNotesResponse = operations['users/featured-notes']['responses']['200']['content']['application/json']; +export type UsersListsCreateRequest = operations['users/lists/create']['requestBody']['content']['application/json']; +export type UsersListsCreateResponse = operations['users/lists/create']['responses']['200']['content']['application/json']; +export type UsersListsDeleteRequest = operations['users/lists/delete']['requestBody']['content']['application/json']; +export type UsersListsListRequest = operations['users/lists/list']['requestBody']['content']['application/json']; +export type UsersListsListResponse = operations['users/lists/list']['responses']['200']['content']['application/json']; +export type UsersListsPullRequest = operations['users/lists/pull']['requestBody']['content']['application/json']; +export type UsersListsPushRequest = operations['users/lists/push']['requestBody']['content']['application/json']; +export type UsersListsShowRequest = operations['users/lists/show']['requestBody']['content']['application/json']; +export type UsersListsShowResponse = operations['users/lists/show']['responses']['200']['content']['application/json']; +export type UsersListsFavoriteRequest = operations['users/lists/favorite']['requestBody']['content']['application/json']; +export type UsersListsUnfavoriteRequest = operations['users/lists/unfavorite']['requestBody']['content']['application/json']; +export type UsersListsUpdateRequest = operations['users/lists/update']['requestBody']['content']['application/json']; +export type UsersListsUpdateResponse = operations['users/lists/update']['responses']['200']['content']['application/json']; +export type UsersListsCreateFromPublicRequest = operations['users/lists/create-from-public']['requestBody']['content']['application/json']; +export type UsersListsCreateFromPublicResponse = operations['users/lists/create-from-public']['responses']['200']['content']['application/json']; +export type UsersListsUpdateMembershipRequest = operations['users/lists/update-membership']['requestBody']['content']['application/json']; +export type UsersListsGetMembershipsRequest = operations['users/lists/get-memberships']['requestBody']['content']['application/json']; +export type UsersNotesRequest = operations['users/notes']['requestBody']['content']['application/json']; +export type UsersNotesResponse = operations['users/notes']['responses']['200']['content']['application/json']; +export type UsersPagesRequest = operations['users/pages']['requestBody']['content']['application/json']; +export type UsersPagesResponse = operations['users/pages']['responses']['200']['content']['application/json']; +export type UsersFlashsRequest = operations['users/flashs']['requestBody']['content']['application/json']; +export type UsersFlashsResponse = operations['users/flashs']['responses']['200']['content']['application/json']; +export type UsersReactionsRequest = operations['users/reactions']['requestBody']['content']['application/json']; +export type UsersReactionsResponse = operations['users/reactions']['responses']['200']['content']['application/json']; +export type UsersRecommendationRequest = operations['users/recommendation']['requestBody']['content']['application/json']; +export type UsersRecommendationResponse = operations['users/recommendation']['responses']['200']['content']['application/json']; +export type UsersRelationRequest = operations['users/relation']['requestBody']['content']['application/json']; +export type UsersRelationResponse = operations['users/relation']['responses']['200']['content']['application/json']; +export type UsersReportAbuseRequest = operations['users/report-abuse']['requestBody']['content']['application/json']; +export type UsersSearchByUsernameAndHostRequest = operations['users/search-by-username-and-host']['requestBody']['content']['application/json']; +export type UsersSearchByUsernameAndHostResponse = operations['users/search-by-username-and-host']['responses']['200']['content']['application/json']; +export type UsersSearchRequest = operations['users/search']['requestBody']['content']['application/json']; +export type UsersSearchResponse = operations['users/search']['responses']['200']['content']['application/json']; +export type UsersShowRequest = operations['users/show']['requestBody']['content']['application/json']; +export type UsersShowResponse = operations['users/show']['responses']['200']['content']['application/json']; +export type UsersAchievementsRequest = operations['users/achievements']['requestBody']['content']['application/json']; +export type UsersUpdateMemoRequest = operations['users/update-memo']['requestBody']['content']['application/json']; +export type FetchRssRequest = operations['fetch-rss']['requestBody']['content']['application/json']; +export type FetchExternalResourcesRequest = operations['fetch-external-resources']['requestBody']['content']['application/json']; +export type RetentionResponse = operations['retention']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts new file mode 100644 index 0000000000..2c25e82d12 --- /dev/null +++ b/packages/misskey-js/src/autogen/models.ts @@ -0,0 +1,42 @@ +/* + * version: 2023.11.0-beta.3 + * generatedAt: 2023-12-08T04:57:48.405Z + */ + +import { components } from './types.js'; +export type Error = components['schemas']['Error']; +export type UserLite = components['schemas']['UserLite']; +export type UserDetailedNotMeOnly = components['schemas']['UserDetailedNotMeOnly']; +export type MeDetailedOnly = components['schemas']['MeDetailedOnly']; +export type UserDetailedNotMe = components['schemas']['UserDetailedNotMe']; +export type MeDetailed = components['schemas']['MeDetailed']; +export type UserDetailed = components['schemas']['UserDetailed']; +export type User = components['schemas']['User']; +export type UserList = components['schemas']['UserList']; +export type Announcement = components['schemas']['Announcement']; +export type App = components['schemas']['App']; +export type Note = components['schemas']['Note']; +export type NoteReaction = components['schemas']['NoteReaction']; +export type NoteFavorite = components['schemas']['NoteFavorite']; +export type Notification = components['schemas']['Notification']; +export type DriveFile = components['schemas']['DriveFile']; +export type DriveFolder = components['schemas']['DriveFolder']; +export type Following = components['schemas']['Following']; +export type Muting = components['schemas']['Muting']; +export type RenoteMuting = components['schemas']['RenoteMuting']; +export type Blocking = components['schemas']['Blocking']; +export type Hashtag = components['schemas']['Hashtag']; +export type InviteCode = components['schemas']['InviteCode']; +export type Page = components['schemas']['Page']; +export type Channel = components['schemas']['Channel']; +export type QueueCount = components['schemas']['QueueCount']; +export type Antenna = components['schemas']['Antenna']; +export type Clip = components['schemas']['Clip']; +export type FederationInstance = components['schemas']['FederationInstance']; +export type GalleryPost = components['schemas']['GalleryPost']; +export type EmojiSimple = components['schemas']['EmojiSimple']; +export type EmojiDetailed = components['schemas']['EmojiDetailed']; +export type Flash = components['schemas']['Flash']; +export type Signin = components['schemas']['Signin']; +export type RoleLite = components['schemas']['RoleLite']; +export type Role = components['schemas']['Role']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts new file mode 100644 index 0000000000..cecc2c872f --- /dev/null +++ b/packages/misskey-js/src/autogen/types.ts @@ -0,0 +1,25038 @@ +/* eslint @typescript-eslint/naming-convention: 0 */ +/* eslint @typescript-eslint/no-explicit-any: 0 */ + +/* + * version: 2023.11.0-beta.3 + * generatedAt: 2023-12-08T04:57:48.142Z + */ + +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +/** OneOf type helpers */ +type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }; +type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U; +type OneOf<T extends any[]> = T extends [infer Only] ? Only : T extends [infer A, infer B, ...infer Rest] ? OneOf<[XOR<A, B>, ...Rest]> : never; + +export type paths = { + '/admin/meta': { + /** + * admin/meta + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/meta']; + }; + '/admin/abuse-user-reports': { + /** + * admin/abuse-user-reports + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/abuse-user-reports']; + }; + '/admin/accounts/create': { + /** + * admin/accounts/create + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['admin/accounts/create']; + }; + '/admin/accounts/delete': { + /** + * admin/accounts/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/accounts/delete']; + }; + '/admin/accounts/find-by-email': { + /** + * admin/accounts/find-by-email + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/accounts/find-by-email']; + }; + '/admin/ad/create': { + /** + * admin/ad/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/ad/create']; + }; + '/admin/ad/delete': { + /** + * admin/ad/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/ad/delete']; + }; + '/admin/ad/list': { + /** + * admin/ad/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/ad/list']; + }; + '/admin/ad/update': { + /** + * admin/ad/update + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/ad/update']; + }; + '/admin/announcements/create': { + /** + * admin/announcements/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/announcements/create']; + }; + '/admin/announcements/delete': { + /** + * admin/announcements/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/announcements/delete']; + }; + '/admin/announcements/list': { + /** + * admin/announcements/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/announcements/list']; + }; + '/admin/announcements/update': { + /** + * admin/announcements/update + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/announcements/update']; + }; + '/admin/avatar-decorations/create': { + /** + * admin/avatar-decorations/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/avatar-decorations/create']; + }; + '/admin/avatar-decorations/delete': { + /** + * admin/avatar-decorations/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/avatar-decorations/delete']; + }; + '/admin/avatar-decorations/list': { + /** + * admin/avatar-decorations/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/avatar-decorations/list']; + }; + '/admin/avatar-decorations/update': { + /** + * admin/avatar-decorations/update + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/avatar-decorations/update']; + }; + '/admin/delete-all-files-of-a-user': { + /** + * admin/delete-all-files-of-a-user + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/delete-all-files-of-a-user']; + }; + '/admin/unset-user-avatar': { + /** + * admin/unset-user-avatar + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/unset-user-avatar']; + }; + '/admin/unset-user-banner': { + /** + * admin/unset-user-banner + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/unset-user-banner']; + }; + '/admin/drive/clean-remote-files': { + /** + * admin/drive/clean-remote-files + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/drive/clean-remote-files']; + }; + '/admin/drive/cleanup': { + /** + * admin/drive/cleanup + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/drive/cleanup']; + }; + '/admin/drive/files': { + /** + * admin/drive/files + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/drive/files']; + }; + '/admin/drive/show-file': { + /** + * admin/drive/show-file + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/drive/show-file']; + }; + '/admin/emoji/add-aliases-bulk': { + /** + * admin/emoji/add-aliases-bulk + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/emoji/add-aliases-bulk']; + }; + '/admin/emoji/add': { + /** + * admin/emoji/add + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/emoji/add']; + }; + '/admin/emoji/copy': { + /** + * admin/emoji/copy + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/emoji/copy']; + }; + '/admin/emoji/delete-bulk': { + /** + * admin/emoji/delete-bulk + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/emoji/delete-bulk']; + }; + '/admin/emoji/delete': { + /** + * admin/emoji/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/emoji/delete']; + }; + '/admin/emoji/import-zip': { + /** + * admin/emoji/import-zip + * @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['admin/emoji/import-zip']; + }; + '/admin/emoji/list-remote': { + /** + * admin/emoji/list-remote + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/emoji/list-remote']; + }; + '/admin/emoji/list': { + /** + * admin/emoji/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/emoji/list']; + }; + '/admin/emoji/remove-aliases-bulk': { + /** + * admin/emoji/remove-aliases-bulk + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/emoji/remove-aliases-bulk']; + }; + '/admin/emoji/set-aliases-bulk': { + /** + * admin/emoji/set-aliases-bulk + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/emoji/set-aliases-bulk']; + }; + '/admin/emoji/set-category-bulk': { + /** + * admin/emoji/set-category-bulk + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/emoji/set-category-bulk']; + }; + '/admin/emoji/set-license-bulk': { + /** + * admin/emoji/set-license-bulk + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/emoji/set-license-bulk']; + }; + '/admin/emoji/update': { + /** + * admin/emoji/update + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/emoji/update']; + }; + '/admin/federation/delete-all-files': { + /** + * admin/federation/delete-all-files + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/federation/delete-all-files']; + }; + '/admin/federation/refresh-remote-instance-metadata': { + /** + * admin/federation/refresh-remote-instance-metadata + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/federation/refresh-remote-instance-metadata']; + }; + '/admin/federation/remove-all-following': { + /** + * admin/federation/remove-all-following + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/federation/remove-all-following']; + }; + '/admin/federation/update-instance': { + /** + * admin/federation/update-instance + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/federation/update-instance']; + }; + '/admin/get-index-stats': { + /** + * admin/get-index-stats + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/get-index-stats']; + }; + '/admin/get-table-stats': { + /** + * admin/get-table-stats + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/get-table-stats']; + }; + '/admin/get-user-ips': { + /** + * admin/get-user-ips + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/get-user-ips']; + }; + '/admin/invite/create': { + /** + * admin/invite/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/invite/create']; + }; + '/admin/invite/list': { + /** + * admin/invite/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/invite/list']; + }; + '/admin/promo/create': { + /** + * admin/promo/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/promo/create']; + }; + '/admin/queue/clear': { + /** + * admin/queue/clear + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/queue/clear']; + }; + '/admin/queue/deliver-delayed': { + /** + * admin/queue/deliver-delayed + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/queue/deliver-delayed']; + }; + '/admin/queue/inbox-delayed': { + /** + * admin/queue/inbox-delayed + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/queue/inbox-delayed']; + }; + '/admin/queue/promote': { + /** + * admin/queue/promote + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/queue/promote']; + }; + '/admin/queue/stats': { + /** + * admin/queue/stats + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/queue/stats']; + }; + '/admin/relays/add': { + /** + * admin/relays/add + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/relays/add']; + }; + '/admin/relays/list': { + /** + * admin/relays/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/relays/list']; + }; + '/admin/relays/remove': { + /** + * admin/relays/remove + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/relays/remove']; + }; + '/admin/reset-password': { + /** + * admin/reset-password + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/reset-password']; + }; + '/admin/resolve-abuse-user-report': { + /** + * admin/resolve-abuse-user-report + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/resolve-abuse-user-report']; + }; + '/admin/send-email': { + /** + * admin/send-email + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/send-email']; + }; + '/admin/server-info': { + /** + * admin/server-info + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/server-info']; + }; + '/admin/show-moderation-logs': { + /** + * admin/show-moderation-logs + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/show-moderation-logs']; + }; + '/admin/show-user': { + /** + * admin/show-user + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/show-user']; + }; + '/admin/show-users': { + /** + * admin/show-users + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/show-users']; + }; + '/admin/suspend-user': { + /** + * admin/suspend-user + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/suspend-user']; + }; + '/admin/unsuspend-user': { + /** + * admin/unsuspend-user + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/unsuspend-user']; + }; + '/admin/update-meta': { + /** + * admin/update-meta + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/update-meta']; + }; + '/admin/delete-account': { + /** + * admin/delete-account + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/delete-account']; + }; + '/admin/update-user-note': { + /** + * admin/update-user-note + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/update-user-note']; + }; + '/admin/roles/create': { + /** + * admin/roles/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/roles/create']; + }; + '/admin/roles/delete': { + /** + * admin/roles/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/roles/delete']; + }; + '/admin/roles/list': { + /** + * admin/roles/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/roles/list']; + }; + '/admin/roles/show': { + /** + * admin/roles/show + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/roles/show']; + }; + '/admin/roles/update': { + /** + * admin/roles/update + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/roles/update']; + }; + '/admin/roles/assign': { + /** + * admin/roles/assign + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/roles/assign']; + }; + '/admin/roles/unassign': { + /** + * admin/roles/unassign + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/roles/unassign']; + }; + '/admin/roles/update-default-policies': { + /** + * admin/roles/update-default-policies + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['admin/roles/update-default-policies']; + }; + '/admin/roles/users': { + /** + * admin/roles/users + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['admin/roles/users']; + }; + '/announcements': { + /** + * announcements + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['announcements']; + }; + '/antennas/create': { + /** + * antennas/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['antennas/create']; + }; + '/antennas/delete': { + /** + * antennas/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['antennas/delete']; + }; + '/antennas/list': { + /** + * antennas/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['antennas/list']; + }; + '/antennas/notes': { + /** + * antennas/notes + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['antennas/notes']; + }; + '/antennas/show': { + /** + * antennas/show + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['antennas/show']; + }; + '/antennas/update': { + /** + * antennas/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['antennas/update']; + }; + '/ap/get': { + /** + * ap/get + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['ap/get']; + }; + '/ap/show': { + /** + * ap/show + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['ap/show']; + }; + '/app/create': { + /** + * app/create + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['app/create']; + }; + '/app/show': { + /** + * app/show + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['app/show']; + }; + '/auth/accept': { + /** + * auth/accept + * @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['auth/accept']; + }; + '/auth/session/generate': { + /** + * auth/session/generate + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['auth/session/generate']; + }; + '/auth/session/show': { + /** + * auth/session/show + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['auth/session/show']; + }; + '/auth/session/userkey': { + /** + * auth/session/userkey + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['auth/session/userkey']; + }; + '/blocking/create': { + /** + * blocking/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:blocks* + */ + post: operations['blocking/create']; + }; + '/blocking/delete': { + /** + * blocking/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:blocks* + */ + post: operations['blocking/delete']; + }; + '/blocking/list': { + /** + * blocking/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:blocks* + */ + post: operations['blocking/list']; + }; + '/channels/create': { + /** + * channels/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + post: operations['channels/create']; + }; + '/channels/featured': { + /** + * channels/featured + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['channels/featured']; + }; + '/channels/follow': { + /** + * channels/follow + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + post: operations['channels/follow']; + }; + '/channels/followed': { + /** + * channels/followed + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:channels* + */ + post: operations['channels/followed']; + }; + '/channels/owned': { + /** + * channels/owned + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:channels* + */ + post: operations['channels/owned']; + }; + '/channels/show': { + /** + * channels/show + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['channels/show']; + }; + '/channels/timeline': { + /** + * channels/timeline + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['channels/timeline']; + }; + '/channels/unfollow': { + /** + * channels/unfollow + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + post: operations['channels/unfollow']; + }; + '/channels/update': { + /** + * channels/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + post: operations['channels/update']; + }; + '/channels/favorite': { + /** + * channels/favorite + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + post: operations['channels/favorite']; + }; + '/channels/unfavorite': { + /** + * channels/unfavorite + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + post: operations['channels/unfavorite']; + }; + '/channels/my-favorites': { + /** + * channels/my-favorites + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:channels* + */ + post: operations['channels/my-favorites']; + }; + '/channels/search': { + /** + * channels/search + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['channels/search']; + }; + '/charts/active-users': { + /** + * charts/active-users + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['charts/active-users']; + /** + * charts/active-users + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['charts/active-users']; + }; + '/charts/ap-request': { + /** + * charts/ap-request + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['charts/ap-request']; + /** + * charts/ap-request + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['charts/ap-request']; + }; + '/charts/drive': { + /** + * charts/drive + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['charts/drive']; + /** + * charts/drive + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['charts/drive']; + }; + '/charts/federation': { + /** + * charts/federation + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['charts/federation']; + /** + * charts/federation + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['charts/federation']; + }; + '/charts/instance': { + /** + * charts/instance + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['charts/instance']; + /** + * charts/instance + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['charts/instance']; + }; + '/charts/notes': { + /** + * charts/notes + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['charts/notes']; + /** + * charts/notes + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['charts/notes']; + }; + '/charts/user/drive': { + /** + * charts/user/drive + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['charts/user/drive']; + /** + * charts/user/drive + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['charts/user/drive']; + }; + '/charts/user/following': { + /** + * charts/user/following + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['charts/user/following']; + /** + * charts/user/following + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['charts/user/following']; + }; + '/charts/user/notes': { + /** + * charts/user/notes + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['charts/user/notes']; + /** + * charts/user/notes + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['charts/user/notes']; + }; + '/charts/user/pv': { + /** + * charts/user/pv + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['charts/user/pv']; + /** + * charts/user/pv + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['charts/user/pv']; + }; + '/charts/user/reactions': { + /** + * charts/user/reactions + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['charts/user/reactions']; + /** + * charts/user/reactions + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['charts/user/reactions']; + }; + '/charts/users': { + /** + * charts/users + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['charts/users']; + /** + * charts/users + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['charts/users']; + }; + '/clips/add-note': { + /** + * clips/add-note + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['clips/add-note']; + }; + '/clips/remove-note': { + /** + * clips/remove-note + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['clips/remove-note']; + }; + '/clips/create': { + /** + * clips/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['clips/create']; + }; + '/clips/delete': { + /** + * clips/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['clips/delete']; + }; + '/clips/list': { + /** + * clips/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['clips/list']; + }; + '/clips/notes': { + /** + * clips/notes + * @description No description provided. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + post: operations['clips/notes']; + }; + '/clips/show': { + /** + * clips/show + * @description No description provided. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + post: operations['clips/show']; + }; + '/clips/update': { + /** + * clips/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['clips/update']; + }; + '/clips/favorite': { + /** + * clips/favorite + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:clip-favorite* + */ + post: operations['clips/favorite']; + }; + '/clips/unfavorite': { + /** + * clips/unfavorite + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:clip-favorite* + */ + post: operations['clips/unfavorite']; + }; + '/clips/my-favorites': { + /** + * clips/my-favorites + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:clip-favorite* + */ + post: operations['clips/my-favorites']; + }; + '/drive': { + /** + * drive + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive']; + }; + '/drive/files': { + /** + * drive/files + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive/files']; + }; + '/drive/files/attached-notes': { + /** + * drive/files/attached-notes + * @description Find the notes to which the given file is attached. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive/files/attached-notes']; + }; + '/drive/files/check-existence': { + /** + * drive/files/check-existence + * @description Check if a given file exists. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive/files/check-existence']; + }; + '/drive/files/create': { + /** + * drive/files/create + * @description Upload a new drive file. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + post: operations['drive/files/create']; + }; + '/drive/files/delete': { + /** + * drive/files/delete + * @description Delete an existing drive file. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + post: operations['drive/files/delete']; + }; + '/drive/files/find-by-hash': { + /** + * drive/files/find-by-hash + * @description Search for a drive file by a hash of the contents. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive/files/find-by-hash']; + }; + '/drive/files/find': { + /** + * drive/files/find + * @description Search for a drive file by the given parameters. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive/files/find']; + }; + '/drive/files/show': { + /** + * drive/files/show + * @description Show the properties of a drive file. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive/files/show']; + }; + '/drive/files/update': { + /** + * drive/files/update + * @description Update the properties of a drive file. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + post: operations['drive/files/update']; + }; + '/drive/files/upload-from-url': { + /** + * drive/files/upload-from-url + * @description Request the server to download a new drive file from the specified URL. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + post: operations['drive/files/upload-from-url']; + }; + '/drive/folders': { + /** + * drive/folders + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive/folders']; + }; + '/drive/folders/create': { + /** + * drive/folders/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + post: operations['drive/folders/create']; + }; + '/drive/folders/delete': { + /** + * drive/folders/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + post: operations['drive/folders/delete']; + }; + '/drive/folders/find': { + /** + * drive/folders/find + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive/folders/find']; + }; + '/drive/folders/show': { + /** + * drive/folders/show + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive/folders/show']; + }; + '/drive/folders/update': { + /** + * drive/folders/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + post: operations['drive/folders/update']; + }; + '/drive/stream': { + /** + * drive/stream + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive/stream']; + }; + '/email-address/available': { + /** + * email-address/available + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['email-address/available']; + }; + '/endpoint': { + /** + * endpoint + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['endpoint']; + }; + '/endpoints': { + /** + * endpoints + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['endpoints']; + }; + '/export-custom-emojis': { + /** + * export-custom-emojis + * @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['export-custom-emojis']; + }; + '/federation/followers': { + /** + * federation/followers + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['federation/followers']; + }; + '/federation/following': { + /** + * federation/following + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['federation/following']; + }; + '/federation/instances': { + /** + * federation/instances + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['federation/instances']; + /** + * federation/instances + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['federation/instances']; + }; + '/federation/show-instance': { + /** + * federation/show-instance + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['federation/show-instance']; + }; + '/federation/update-remote-user': { + /** + * federation/update-remote-user + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['federation/update-remote-user']; + }; + '/federation/users': { + /** + * federation/users + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['federation/users']; + }; + '/federation/stats': { + /** + * federation/stats + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['federation/stats']; + /** + * federation/stats + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['federation/stats']; + }; + '/following/create': { + /** + * following/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + post: operations['following/create']; + }; + '/following/delete': { + /** + * following/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + post: operations['following/delete']; + }; + '/following/update': { + /** + * following/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + post: operations['following/update']; + }; + '/following/update-all': { + /** + * following/update-all + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + post: operations['following/update-all']; + }; + '/following/invalidate': { + /** + * following/invalidate + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + post: operations['following/invalidate']; + }; + '/following/requests/accept': { + /** + * following/requests/accept + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + post: operations['following/requests/accept']; + }; + '/following/requests/cancel': { + /** + * following/requests/cancel + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + post: operations['following/requests/cancel']; + }; + '/following/requests/list': { + /** + * following/requests/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:following* + */ + post: operations['following/requests/list']; + }; + '/following/requests/reject': { + /** + * following/requests/reject + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + post: operations['following/requests/reject']; + }; + '/gallery/featured': { + /** + * gallery/featured + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['gallery/featured']; + }; + '/gallery/popular': { + /** + * gallery/popular + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['gallery/popular']; + }; + '/gallery/posts': { + /** + * gallery/posts + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['gallery/posts']; + }; + '/gallery/posts/create': { + /** + * gallery/posts/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery* + */ + post: operations['gallery/posts/create']; + }; + '/gallery/posts/delete': { + /** + * gallery/posts/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery* + */ + post: operations['gallery/posts/delete']; + }; + '/gallery/posts/like': { + /** + * gallery/posts/like + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery-likes* + */ + post: operations['gallery/posts/like']; + }; + '/gallery/posts/show': { + /** + * gallery/posts/show + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['gallery/posts/show']; + }; + '/gallery/posts/unlike': { + /** + * gallery/posts/unlike + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery-likes* + */ + post: operations['gallery/posts/unlike']; + }; + '/gallery/posts/update': { + /** + * gallery/posts/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery* + */ + post: operations['gallery/posts/update']; + }; + '/get-online-users-count': { + /** + * get-online-users-count + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['get-online-users-count']; + /** + * get-online-users-count + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['get-online-users-count']; + }; + '/get-avatar-decorations': { + /** + * get-avatar-decorations + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['get-avatar-decorations']; + }; + '/hashtags/list': { + /** + * hashtags/list + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['hashtags/list']; + }; + '/hashtags/search': { + /** + * hashtags/search + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['hashtags/search']; + }; + '/hashtags/show': { + /** + * hashtags/show + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['hashtags/show']; + }; + '/hashtags/trend': { + /** + * hashtags/trend + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['hashtags/trend']; + /** + * hashtags/trend + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['hashtags/trend']; + }; + '/hashtags/users': { + /** + * hashtags/users + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['hashtags/users']; + }; + '/i': { + /** + * i + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['i']; + }; + '/i/2fa/done': { + /** + * i/2fa/done + * @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/2fa/done']; + }; + '/i/2fa/key-done': { + /** + * i/2fa/key-done + * @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/2fa/key-done']; + }; + '/i/2fa/password-less': { + /** + * i/2fa/password-less + * @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/2fa/password-less']; + }; + '/i/2fa/register-key': { + /** + * i/2fa/register-key + * @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/2fa/register-key']; + }; + '/i/2fa/register': { + /** + * i/2fa/register + * @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/2fa/register']; + }; + '/i/2fa/update-key': { + /** + * i/2fa/update-key + * @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/2fa/update-key']; + }; + '/i/2fa/remove-key': { + /** + * i/2fa/remove-key + * @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/2fa/remove-key']; + }; + '/i/2fa/unregister': { + /** + * i/2fa/unregister + * @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/2fa/unregister']; + }; + '/i/apps': { + /** + * i/apps + * @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/apps']; + }; + '/i/authorized-apps': { + /** + * i/authorized-apps + * @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/authorized-apps']; + }; + '/i/claim-achievement': { + /** + * i/claim-achievement + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['i/claim-achievement']; + }; + '/i/change-password': { + /** + * i/change-password + * @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/change-password']; + }; + '/i/delete-account': { + /** + * i/delete-account + * @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/delete-account']; + }; + '/i/export-blocking': { + /** + * i/export-blocking + * @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-blocking']; + }; + '/i/export-following': { + /** + * i/export-following + * @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-following']; + }; + '/i/export-mute': { + /** + * i/export-mute + * @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-mute']; + }; + '/i/export-notes': { + /** + * i/export-notes + * @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-notes']; + }; + '/i/export-favorites': { + /** + * i/export-favorites + * @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-favorites']; + }; + '/i/export-user-lists': { + /** + * i/export-user-lists + * @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-user-lists']; + }; + '/i/export-antennas': { + /** + * i/export-antennas + * @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-antennas']; + }; + '/i/favorites': { + /** + * i/favorites + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:favorites* + */ + post: operations['i/favorites']; + }; + '/i/gallery/likes': { + /** + * i/gallery/likes + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:gallery-likes* + */ + post: operations['i/gallery/likes']; + }; + '/i/gallery/posts': { + /** + * i/gallery/posts + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:gallery* + */ + post: operations['i/gallery/posts']; + }; + '/i/import-blocking': { + /** + * i/import-blocking + * @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/import-blocking']; + }; + '/i/import-following': { + /** + * i/import-following + * @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/import-following']; + }; + '/i/import-muting': { + /** + * i/import-muting + * @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/import-muting']; + }; + '/i/import-user-lists': { + /** + * i/import-user-lists + * @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/import-user-lists']; + }; + '/i/import-antennas': { + /** + * i/import-antennas + * @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/import-antennas']; + }; + '/i/notifications': { + /** + * i/notifications + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:notifications* + */ + post: operations['i/notifications']; + }; + '/i/notifications-grouped': { + /** + * i/notifications-grouped + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:notifications* + */ + post: operations['i/notifications-grouped']; + }; + '/i/page-likes': { + /** + * i/page-likes + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:page-likes* + */ + post: operations['i/page-likes']; + }; + '/i/pages': { + /** + * i/pages + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:pages* + */ + post: operations['i/pages']; + }; + '/i/pin': { + /** + * i/pin + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['i/pin']; + }; + '/i/read-all-unread-notes': { + /** + * i/read-all-unread-notes + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['i/read-all-unread-notes']; + }; + '/i/read-announcement': { + /** + * i/read-announcement + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['i/read-announcement']; + }; + '/i/regenerate-token': { + /** + * i/regenerate-token + * @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/regenerate-token']; + }; + '/i/registry/get-all': { + /** + * i/registry/get-all + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['i/registry/get-all']; + }; + '/i/registry/get-detail': { + /** + * i/registry/get-detail + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['i/registry/get-detail']; + }; + '/i/registry/get': { + /** + * i/registry/get + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['i/registry/get']; + }; + '/i/registry/keys-with-type': { + /** + * i/registry/keys-with-type + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['i/registry/keys-with-type']; + }; + '/i/registry/keys': { + /** + * i/registry/keys + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['i/registry/keys']; + }; + '/i/registry/remove': { + /** + * i/registry/remove + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['i/registry/remove']; + }; + '/i/registry/scopes-with-domain': { + /** + * i/registry/scopes-with-domain + * @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/registry/scopes-with-domain']; + }; + '/i/registry/set': { + /** + * i/registry/set + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['i/registry/set']; + }; + '/i/revoke-token': { + /** + * i/revoke-token + * @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/revoke-token']; + }; + '/i/signin-history': { + /** + * i/signin-history + * @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/signin-history']; + }; + '/i/unpin': { + /** + * i/unpin + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['i/unpin']; + }; + '/i/update-email': { + /** + * i/update-email + * @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/update-email']; + }; + '/i/update': { + /** + * i/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['i/update']; + }; + '/i/move': { + /** + * i/move + * @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/move']; + }; + '/i/webhooks/create': { + /** + * i/webhooks/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['i/webhooks/create']; + }; + '/i/webhooks/list': { + /** + * i/webhooks/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['i/webhooks/list']; + }; + '/i/webhooks/show': { + /** + * i/webhooks/show + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['i/webhooks/show']; + }; + '/i/webhooks/update': { + /** + * i/webhooks/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['i/webhooks/update']; + }; + '/i/webhooks/delete': { + /** + * i/webhooks/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['i/webhooks/delete']; + }; + '/invite/create': { + /** + * invite/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['invite/create']; + }; + '/invite/delete': { + /** + * invite/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['invite/delete']; + }; + '/invite/list': { + /** + * invite/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['invite/list']; + }; + '/invite/limit': { + /** + * invite/limit + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['invite/limit']; + }; + '/meta': { + /** + * meta + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['meta']; + }; + '/emojis': { + /** + * emojis + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['emojis']; + /** + * emojis + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['emojis']; + }; + '/emoji': { + /** + * emoji + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['emoji']; + /** + * emoji + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['emoji']; + }; + '/miauth/gen-token': { + /** + * miauth/gen-token + * @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['miauth/gen-token']; + }; + '/mute/create': { + /** + * mute/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:mutes* + */ + post: operations['mute/create']; + }; + '/mute/delete': { + /** + * mute/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:mutes* + */ + post: operations['mute/delete']; + }; + '/mute/list': { + /** + * mute/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:mutes* + */ + post: operations['mute/list']; + }; + '/renote-mute/create': { + /** + * renote-mute/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:mutes* + */ + post: operations['renote-mute/create']; + }; + '/renote-mute/delete': { + /** + * renote-mute/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:mutes* + */ + post: operations['renote-mute/delete']; + }; + '/renote-mute/list': { + /** + * renote-mute/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:mutes* + */ + post: operations['renote-mute/list']; + }; + '/my/apps': { + /** + * my/apps + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['my/apps']; + }; + '/notes': { + /** + * notes + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes']; + }; + '/notes/children': { + /** + * notes/children + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/children']; + }; + '/notes/clips': { + /** + * notes/clips + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/clips']; + }; + '/notes/conversation': { + /** + * notes/conversation + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/conversation']; + }; + '/notes/create': { + /** + * notes/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + post: operations['notes/create']; + }; + '/notes/delete': { + /** + * notes/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + post: operations['notes/delete']; + }; + '/notes/favorites/create': { + /** + * notes/favorites/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:favorites* + */ + post: operations['notes/favorites/create']; + }; + '/notes/favorites/delete': { + /** + * notes/favorites/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:favorites* + */ + post: operations['notes/favorites/delete']; + }; + '/notes/featured': { + /** + * notes/featured + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['notes/featured']; + /** + * notes/featured + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/featured']; + }; + '/notes/global-timeline': { + /** + * notes/global-timeline + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/global-timeline']; + }; + '/notes/hybrid-timeline': { + /** + * notes/hybrid-timeline + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['notes/hybrid-timeline']; + }; + '/notes/local-timeline': { + /** + * notes/local-timeline + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/local-timeline']; + }; + '/notes/mentions': { + /** + * notes/mentions + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['notes/mentions']; + }; + '/notes/polls/recommendation': { + /** + * notes/polls/recommendation + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['notes/polls/recommendation']; + }; + '/notes/polls/vote': { + /** + * notes/polls/vote + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:votes* + */ + post: operations['notes/polls/vote']; + }; + '/notes/reactions': { + /** + * notes/reactions + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['notes/reactions']; + /** + * notes/reactions + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/reactions']; + }; + '/notes/reactions/create': { + /** + * notes/reactions/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:reactions* + */ + post: operations['notes/reactions/create']; + }; + '/notes/reactions/delete': { + /** + * notes/reactions/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:reactions* + */ + post: operations['notes/reactions/delete']; + }; + '/notes/renotes': { + /** + * notes/renotes + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/renotes']; + }; + '/notes/replies': { + /** + * notes/replies + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/replies']; + }; + '/notes/search-by-tag': { + /** + * notes/search-by-tag + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/search-by-tag']; + }; + '/notes/search': { + /** + * notes/search + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/search']; + }; + '/notes/show': { + /** + * notes/show + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/show']; + }; + '/notes/state': { + /** + * notes/state + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['notes/state']; + }; + '/notes/thread-muting/create': { + /** + * notes/thread-muting/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['notes/thread-muting/create']; + }; + '/notes/thread-muting/delete': { + /** + * notes/thread-muting/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['notes/thread-muting/delete']; + }; + '/notes/timeline': { + /** + * notes/timeline + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['notes/timeline']; + }; + '/notes/translate': { + /** + * notes/translate + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['notes/translate']; + }; + '/notes/unrenote': { + /** + * notes/unrenote + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + post: operations['notes/unrenote']; + }; + '/notes/user-list-timeline': { + /** + * notes/user-list-timeline + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['notes/user-list-timeline']; + }; + '/notifications/create': { + /** + * notifications/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notifications* + */ + post: operations['notifications/create']; + }; + '/notifications/mark-all-as-read': { + /** + * notifications/mark-all-as-read + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notifications* + */ + post: operations['notifications/mark-all-as-read']; + }; + '/notifications/test-notification': { + /** + * notifications/test-notification + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notifications* + */ + post: operations['notifications/test-notification']; + }; + '/page-push': { + /** + * page-push + * @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['page-push']; + }; + '/pages/create': { + /** + * pages/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:pages* + */ + post: operations['pages/create']; + }; + '/pages/delete': { + /** + * pages/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:pages* + */ + post: operations['pages/delete']; + }; + '/pages/featured': { + /** + * pages/featured + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['pages/featured']; + }; + '/pages/like': { + /** + * pages/like + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:page-likes* + */ + post: operations['pages/like']; + }; + '/pages/show': { + /** + * pages/show + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['pages/show']; + }; + '/pages/unlike': { + /** + * pages/unlike + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:page-likes* + */ + post: operations['pages/unlike']; + }; + '/pages/update': { + /** + * pages/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:pages* + */ + post: operations['pages/update']; + }; + '/flash/create': { + /** + * flash/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash* + */ + post: operations['flash/create']; + }; + '/flash/delete': { + /** + * flash/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash* + */ + post: operations['flash/delete']; + }; + '/flash/featured': { + /** + * flash/featured + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['flash/featured']; + }; + '/flash/like': { + /** + * flash/like + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash-likes* + */ + post: operations['flash/like']; + }; + '/flash/show': { + /** + * flash/show + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['flash/show']; + }; + '/flash/unlike': { + /** + * flash/unlike + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash-likes* + */ + post: operations['flash/unlike']; + }; + '/flash/update': { + /** + * flash/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash* + */ + post: operations['flash/update']; + }; + '/flash/my': { + /** + * flash/my + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:flash* + */ + post: operations['flash/my']; + }; + '/flash/my-likes': { + /** + * flash/my-likes + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:flash-likes* + */ + post: operations['flash/my-likes']; + }; + '/ping': { + /** + * ping + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['ping']; + }; + '/pinned-users': { + /** + * pinned-users + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['pinned-users']; + }; + '/promo/read': { + /** + * promo/read + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['promo/read']; + }; + '/roles/list': { + /** + * roles/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['roles/list']; + }; + '/roles/show': { + /** + * roles/show + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['roles/show']; + }; + '/roles/users': { + /** + * roles/users + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['roles/users']; + }; + '/roles/notes': { + /** + * roles/notes + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['roles/notes']; + }; + '/request-reset-password': { + /** + * request-reset-password + * @description Request a users password to be reset. + * + * **Credential required**: *No* + */ + post: operations['request-reset-password']; + }; + '/reset-db': { + /** + * reset-db + * @description Only available when running with <code>NODE_ENV=testing</code>. Reset the database and flush Redis. + * + * **Credential required**: *No* + */ + post: operations['reset-db']; + }; + '/reset-password': { + /** + * reset-password + * @description Complete the password reset that was previously requested. + * + * **Credential required**: *No* + */ + post: operations['reset-password']; + }; + '/server-info': { + /** + * server-info + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['server-info']; + /** + * server-info + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['server-info']; + }; + '/stats': { + /** + * stats + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['stats']; + }; + '/sw/show-registration': { + /** + * sw/show-registration + * @description Check push notification registration exists. + * + * **Credential required**: *Yes* + */ + post: operations['sw/show-registration']; + }; + '/sw/update-registration': { + /** + * sw/update-registration + * @description Update push notification registration. + * + * **Credential required**: *Yes* + */ + post: operations['sw/update-registration']; + }; + '/sw/register': { + /** + * sw/register + * @description Register to receive push notifications. + * + * **Credential required**: *Yes* + */ + post: operations['sw/register']; + }; + '/sw/unregister': { + /** + * sw/unregister + * @description Unregister from receiving push notifications. + * + * **Credential required**: *No* + */ + post: operations['sw/unregister']; + }; + '/test': { + /** + * test + * @description Endpoint for testing input validation. + * + * **Credential required**: *No* + */ + post: operations['test']; + }; + '/username/available': { + /** + * username/available + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['username/available']; + }; + '/users': { + /** + * users + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['users']; + }; + '/users/clips': { + /** + * users/clips + * @description Show all clips this user owns. + * + * **Credential required**: *No* + */ + post: operations['users/clips']; + }; + '/users/followers': { + /** + * users/followers + * @description Show everyone that follows this user. + * + * **Credential required**: *No* + */ + post: operations['users/followers']; + }; + '/users/following': { + /** + * users/following + * @description Show everyone that this user is following. + * + * **Credential required**: *No* + */ + post: operations['users/following']; + }; + '/users/gallery/posts': { + /** + * users/gallery/posts + * @description Show all gallery posts by the given user. + * + * **Credential required**: *No* + */ + post: operations['users/gallery/posts']; + }; + '/users/get-frequently-replied-users': { + /** + * users/get-frequently-replied-users + * @description Get a list of other users that the specified user frequently replies to. + * + * **Credential required**: *No* + */ + post: operations['users/get-frequently-replied-users']; + }; + '/users/featured-notes': { + /** + * users/featured-notes + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['users/featured-notes']; + /** + * users/featured-notes + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['users/featured-notes']; + }; + '/users/lists/create': { + /** + * users/lists/create + * @description Create a new list of users. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['users/lists/create']; + }; + '/users/lists/delete': { + /** + * users/lists/delete + * @description Delete an existing list of users. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['users/lists/delete']; + }; + '/users/lists/list': { + /** + * users/lists/list + * @description Show all lists that the authenticated user has created. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + post: operations['users/lists/list']; + }; + '/users/lists/pull': { + /** + * users/lists/pull + * @description Remove a user from a list. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['users/lists/pull']; + }; + '/users/lists/push': { + /** + * users/lists/push + * @description Add a user to an existing list. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['users/lists/push']; + }; + '/users/lists/show': { + /** + * users/lists/show + * @description Show the properties of a list. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + post: operations['users/lists/show']; + }; + '/users/lists/favorite': { + /** + * users/lists/favorite + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['users/lists/favorite']; + }; + '/users/lists/unfavorite': { + /** + * users/lists/unfavorite + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['users/lists/unfavorite']; + }; + '/users/lists/update': { + /** + * users/lists/update + * @description Update the properties of a list. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['users/lists/update']; + }; + '/users/lists/create-from-public': { + /** + * users/lists/create-from-public + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['users/lists/create-from-public']; + }; + '/users/lists/update-membership': { + /** + * users/lists/update-membership + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['users/lists/update-membership']; + }; + '/users/lists/get-memberships': { + /** + * users/lists/get-memberships + * @description No description provided. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + post: operations['users/lists/get-memberships']; + }; + '/users/notes': { + /** + * users/notes + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['users/notes']; + }; + '/users/pages': { + /** + * users/pages + * @description Show all pages this user created. + * + * **Credential required**: *No* + */ + post: operations['users/pages']; + }; + '/users/flashs': { + /** + * users/flashs + * @description Show all flashs this user created. + * + * **Credential required**: *No* + */ + post: operations['users/flashs']; + }; + '/users/reactions': { + /** + * users/reactions + * @description Show all reactions this user made. + * + * **Credential required**: *No* + */ + post: operations['users/reactions']; + }; + '/users/recommendation': { + /** + * users/recommendation + * @description Show users that the authenticated user might be interested to follow. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['users/recommendation']; + }; + '/users/relation': { + /** + * users/relation + * @description Show the different kinds of relations between the authenticated user and the specified user(s). + * + * **Credential required**: *Yes* + */ + post: operations['users/relation']; + }; + '/users/report-abuse': { + /** + * users/report-abuse + * @description File a report. + * + * **Credential required**: *Yes* + */ + post: operations['users/report-abuse']; + }; + '/users/search-by-username-and-host': { + /** + * users/search-by-username-and-host + * @description Search for a user by username and/or host. + * + * **Credential required**: *No* + */ + post: operations['users/search-by-username-and-host']; + }; + '/users/search': { + /** + * users/search + * @description Search for users. + * + * **Credential required**: *No* + */ + post: operations['users/search']; + }; + '/users/show': { + /** + * users/show + * @description Show the properties of a user. + * + * **Credential required**: *No* + */ + post: operations['users/show']; + }; + '/users/achievements': { + /** + * users/achievements + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['users/achievements']; + }; + '/users/update-memo': { + /** + * users/update-memo + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['users/update-memo']; + }; + '/fetch-rss': { + /** + * fetch-rss + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['fetch-rss']; + /** + * fetch-rss + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['fetch-rss']; + }; + '/fetch-external-resources': { + /** + * fetch-external-resources + * @description No description provided. + * + * **Credential required**: *Yes* + */ + post: operations['fetch-external-resources']; + }; + '/retention': { + /** + * retention + * @description No description provided. + * + * **Credential required**: *No* + */ + get: operations['retention']; + /** + * retention + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['retention']; + }; +}; + +export type webhooks = Record<string, never>; + +export type components = { + schemas: { + Error: { + /** @description An error object. */ + error: { + /** @description An error code. Unique within the endpoint. */ + code: string; + /** @description An error message. */ + message: string; + /** + * Format: uuid + * @description An error ID. This ID is static. + */ + id: string; + }; + }; + UserLite: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** @example 藍 */ + name: string | null; + /** @example ai */ + username: string; + /** + * @description The local host is represented with `null`. + * @example misskey.example.com + */ + host: string | null; + /** Format: url */ + avatarUrl: string | null; + avatarBlurhash: string | null; + avatarDecorations: { + /** Format: id */ + id: string; + angle?: number; + flipH?: boolean; + /** Format: url */ + url: string; + }[]; + isBot?: boolean; + isCat?: boolean; + instance?: { + name: string | null; + softwareName: string | null; + softwareVersion: string | null; + iconUrl: string | null; + faviconUrl: string | null; + themeColor: string | null; + }; + emojis: Record<string, never>; + /** @enum {string} */ + onlineStatus: 'unknown' | 'online' | 'active' | 'offline'; + badgeRoles?: ({ + name: string; + iconUrl: string | null; + displayOrder: number; + })[]; + }; + UserDetailedNotMeOnly: { + /** Format: url */ + url: string | null; + /** Format: uri */ + uri: string | null; + /** Format: uri */ + movedTo: string | null; + alsoKnownAs: string[] | null; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string | null; + /** Format: date-time */ + lastFetchedAt: string | null; + /** Format: url */ + bannerUrl: string | null; + bannerBlurhash: string | null; + isLocked: boolean; + isSilenced: boolean; + /** @example false */ + isSuspended: boolean; + /** @example Hi masters, I am Ai! */ + description: string | null; + location: string | null; + /** @example 2018-03-12 */ + birthday: string | null; + /** @example ja-JP */ + lang: string | null; + fields: { + name: string; + value: string; + }[]; + verifiedLinks: string[]; + followersCount: number; + followingCount: number; + notesCount: number; + pinnedNoteIds: string[]; + pinnedNotes: components['schemas']['Note'][]; + pinnedPageId: string | null; + pinnedPage: components['schemas']['Page'] | null; + publicReactions: boolean; + /** @enum {string} */ + ffVisibility: 'public' | 'followers' | 'private'; + /** @default false */ + twoFactorEnabled: boolean; + /** @default false */ + usePasswordLessLogin: boolean; + /** @default false */ + securityKeys: boolean; + roles: components['schemas']['RoleLite'][]; + memo: string | null; + moderationNote?: string; + isFollowing?: boolean; + isFollowed?: boolean; + hasPendingFollowRequestFromYou?: boolean; + hasPendingFollowRequestToYou?: boolean; + isBlocking?: boolean; + isBlocked?: boolean; + isMuted?: boolean; + isRenoteMuted?: boolean; + /** @enum {string} */ + notify?: 'normal' | 'none'; + withReplies?: boolean; + }; + MeDetailedOnly: { + /** Format: id */ + avatarId: string | null; + /** Format: id */ + bannerId: string | null; + isModerator: boolean | null; + isAdmin: boolean | null; + injectFeaturedNote: boolean; + receiveAnnouncementEmail: boolean; + alwaysMarkNsfw: boolean; + autoSensitive: boolean; + carefulBot: boolean; + autoAcceptFollowed: boolean; + noCrawle: boolean; + preventAiLearning: boolean; + isExplorable: boolean; + isDeleted: boolean; + /** @enum {string} */ + twoFactorBackupCodesStock: 'full' | 'partial' | 'none'; + hideOnlineStatus: boolean; + hasUnreadSpecifiedNotes: boolean; + hasUnreadMentions: boolean; + hasUnreadAnnouncement: boolean; + unreadAnnouncements: components['schemas']['Announcement'][]; + hasUnreadAntenna: boolean; + hasUnreadChannel: boolean; + hasUnreadNotification: boolean; + hasPendingReceivedFollowRequest: boolean; + unreadNotificationsCount: number; + mutedWords: string[][]; + hardMutedWords: string[][]; + mutedInstances: string[] | null; + notificationRecieveConfig: { + app?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + quote?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + reply?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + follow?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + renote?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + mention?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + reaction?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + pollEnded?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + achievementEarned?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + receiveFollowRequest?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + followRequestAccepted?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + }; + emailNotificationTypes: string[]; + achievements: { + name: string; + unlockedAt: number; + }[]; + loggedInDays: number; + policies: { + gtlAvailable: boolean; + ltlAvailable: boolean; + canPublicNote: boolean; + canInvite: boolean; + inviteLimit: number; + inviteLimitCycle: number; + inviteExpirationTime: number; + canManageCustomEmojis: boolean; + canManageAvatarDecorations: boolean; + canSearchNotes: boolean; + canUseTranslator: boolean; + canHideAds: boolean; + driveCapacityMb: number; + alwaysMarkNsfw: boolean; + pinLimit: number; + antennaLimit: number; + wordMuteLimit: number; + webhookLimit: number; + clipLimit: number; + noteEachClipsLimit: number; + userListLimit: number; + userEachUserListsLimit: number; + rateLimitFactor: number; + }; + email?: string | null; + emailVerified?: boolean | null; + securityKeysList?: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + name: string; + /** Format: date-time */ + lastUsed: string; + }[]; + }; + UserDetailedNotMe: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly']; + MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly']; + UserDetailed: components['schemas']['UserDetailedNotMe'] | components['schemas']['MeDetailed']; + User: components['schemas']['UserLite'] | components['schemas']['UserDetailed'] | components['schemas']['UserDetailedNotMe'] | components['schemas']['MeDetailed']; + UserList: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + name: string; + userIds?: string[]; + isPublic: boolean; + }; + Announcement: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string | null; + text: string; + title: string; + imageUrl: string | null; + icon: string; + display: string; + needConfirmationToRead: boolean; + silence: boolean; + forYou: boolean; + isRead?: boolean; + }; + App: { + id: string; + name: string; + callbackUrl: string | null; + permission: string[]; + secret?: string; + isAuthorized?: boolean; + }; + Note: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt?: string | null; + text: string | null; + cw?: string | null; + /** Format: id */ + userId: string; + user: components['schemas']['UserLite']; + /** + * Format: id + * @example xxxxxxxxxx + */ + replyId?: string | null; + /** + * Format: id + * @example xxxxxxxxxx + */ + renoteId?: string | null; + reply?: components['schemas']['Note'] | null; + renote?: components['schemas']['Note'] | null; + isHidden?: boolean; + visibility: string; + mentions?: string[]; + visibleUserIds?: string[]; + fileIds?: string[]; + files?: components['schemas']['DriveFile'][]; + tags?: string[]; + poll?: Record<string, unknown> | null; + /** + * Format: id + * @example xxxxxxxxxx + */ + channelId?: string | null; + channel?: { + id: string; + name: string; + color: string; + isSensitive: boolean; + allowRenoteToExternal: boolean; + } | null; + localOnly?: boolean; + reactionAcceptance: string | null; + reactions: Record<string, never>; + renoteCount: number; + repliesCount: number; + uri?: string; + url?: string; + reactionAndUserPairCache?: string[]; + clippedCount?: number; + myReaction?: Record<string, unknown> | null; + }; + NoteReaction: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + user: components['schemas']['UserLite']; + type: string; + }; + NoteFavorite: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + note: components['schemas']['Note']; + /** Format: id */ + noteId: string; + }; + Notification: { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** @enum {string} */ + type: 'note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped'; + user?: components['schemas']['UserLite'] | null; + /** Format: id */ + userId?: string | null; + note?: components['schemas']['Note'] | null; + reaction?: string | null; + achievement?: string; + body?: string | null; + header?: string | null; + icon?: string | null; + reactions?: { + user: components['schemas']['UserLite']; + reaction: string; + }[] | null; + users?: components['schemas']['UserLite'][] | null; + }; + DriveFile: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** @example lenna.jpg */ + name: string; + /** @example image/jpeg */ + type: string; + /** + * Format: md5 + * @example 15eca7fba0480996e2245f5185bf39f2 + */ + md5: string; + /** @example 51469 */ + size: number; + isSensitive: boolean; + blurhash: string | null; + properties: { + /** @example 1280 */ + width?: number; + /** @example 720 */ + height?: number; + /** @example 8 */ + orientation?: number; + /** @example rgb(40,65,87) */ + avgColor?: string; + }; + /** Format: url */ + url: string; + /** Format: url */ + thumbnailUrl: string | null; + comment: string | null; + /** + * Format: id + * @example xxxxxxxxxx + */ + folderId: string | null; + folder?: components['schemas']['DriveFolder'] | null; + /** + * Format: id + * @example xxxxxxxxxx + */ + userId: string | null; + user?: components['schemas']['UserLite'] | null; + }; + DriveFolder: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + name: string; + /** + * Format: id + * @example xxxxxxxxxx + */ + parentId: string | null; + foldersCount?: number; + filesCount?: number; + parent?: components['schemas']['DriveFolder'] | null; + }; + Following: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: id */ + followeeId: string; + /** Format: id */ + followerId: string; + followee?: components['schemas']['UserDetailed']; + follower?: components['schemas']['UserDetailed']; + }; + Muting: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + expiresAt: string | null; + /** Format: id */ + muteeId: string; + mutee: components['schemas']['UserDetailed']; + }; + RenoteMuting: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: id */ + muteeId: string; + mutee: components['schemas']['UserDetailed']; + }; + Blocking: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: id */ + blockeeId: string; + blockee: components['schemas']['UserDetailed']; + }; + Hashtag: { + /** @example misskey */ + tag: string; + mentionedUsersCount: number; + mentionedLocalUsersCount: number; + mentionedRemoteUsersCount: number; + attachedUsersCount: number; + attachedLocalUsersCount: number; + attachedRemoteUsersCount: number; + }; + InviteCode: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** @example GR6S02ERUA5VR */ + code: string; + /** Format: date-time */ + expiresAt: string | null; + /** Format: date-time */ + createdAt: string; + createdBy: components['schemas']['UserLite'] | null; + usedBy: components['schemas']['UserLite'] | null; + /** Format: date-time */ + usedAt: string | null; + used: boolean; + }; + Page: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + /** Format: id */ + userId: string; + user: components['schemas']['UserLite']; + content: Record<string, never>[]; + variables: Record<string, never>[]; + title: string; + name: string; + summary: string | null; + hideTitleWhenPinned: boolean; + alignCenter: boolean; + font: string; + script: string; + eyeCatchingImageId: string | null; + eyeCatchingImage: components['schemas']['DriveFile'] | null; + attachedFiles: components['schemas']['DriveFile'][]; + likedCount: number; + isLiked?: boolean; + }; + Channel: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + lastNotedAt: string | null; + name: string; + description: string | null; + /** Format: id */ + userId: string | null; + /** Format: url */ + bannerUrl: string | null; + pinnedNoteIds: string[]; + color: string; + isArchived: boolean; + usersCount: number; + notesCount: number; + isSensitive: boolean; + allowRenoteToExternal: boolean; + isFollowing?: boolean; + isFavorited?: boolean; + pinnedNotes?: components['schemas']['Note'][]; + }; + QueueCount: { + waiting: number; + active: number; + completed: number; + failed: number; + delayed: number; + }; + Antenna: { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + name: string; + keywords: string[][]; + excludeKeywords: string[][]; + /** @enum {string} */ + src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist'; + /** Format: id */ + userListId: string | null; + users: string[]; + /** @default false */ + caseSensitive: boolean; + /** @default false */ + localOnly: boolean; + notify: boolean; + /** @default false */ + withReplies: boolean; + withFile: boolean; + isActive: boolean; + /** @default false */ + hasUnreadNote: boolean; + }; + Clip: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + lastClippedAt: string | null; + /** Format: id */ + userId: string; + user: components['schemas']['UserLite']; + name: string; + description: string | null; + isPublic: boolean; + favoritedCount: number; + isFavorited?: boolean; + }; + FederationInstance: { + /** Format: id */ + id: string; + /** Format: date-time */ + firstRetrievedAt: string; + /** @example misskey.example.com */ + host: string; + usersCount: number; + notesCount: number; + followingCount: number; + followersCount: number; + isNotResponding: boolean; + isSuspended: boolean; + isBlocked: boolean; + /** @example misskey */ + softwareName: string | null; + softwareVersion: string | null; + /** @example true */ + openRegistrations: boolean | null; + name: string | null; + description: string | null; + maintainerName: string | null; + maintainerEmail: string | null; + isSilenced: boolean; + /** Format: url */ + iconUrl: string | null; + /** Format: url */ + faviconUrl: string | null; + themeColor: string | null; + /** Format: date-time */ + infoUpdatedAt: string | null; + /** Format: date-time */ + latestRequestReceivedAt: string | null; + }; + GalleryPost: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + /** Format: id */ + userId: string; + user: components['schemas']['UserLite']; + title: string; + description: string | null; + fileIds?: string[]; + files?: components['schemas']['DriveFile'][]; + tags?: string[]; + isSensitive: boolean; + likedCount: number; + isLiked?: boolean; + }; + EmojiSimple: { + aliases: string[]; + name: string; + category: string | null; + url: string; + isSensitive?: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; + }; + EmojiDetailed: { + /** Format: id */ + id: string; + aliases: string[]; + name: string; + category: string | null; + /** @description The local host is represented with `null`. */ + host: string | null; + url: string; + license: string | null; + isSensitive: boolean; + localOnly: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; + }; + Flash: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + /** Format: id */ + userId: string; + user: components['schemas']['UserLite']; + title: string; + summary: string; + script: string; + likedCount: number | null; + isLiked?: boolean; + }; + Signin: { + id: string; + /** Format: date-time */ + createdAt: string; + ip: string; + headers: Record<string, never>; + success: boolean; + }; + RoleLite: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** @example New Role */ + name: string; + /** @example #000000 */ + color: string | null; + iconUrl: string | null; + description: string; + /** @example false */ + isModerator: boolean; + /** @example false */ + isAdministrator: boolean; + /** @example 0 */ + displayOrder: number; + }; + Role: components['schemas']['RoleLite'] & ({ + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + /** @enum {string} */ + target: 'manual' | 'conditional'; + condFormula: Record<string, never>; + /** @example false */ + isPublic: boolean; + /** @example false */ + isExplorable: boolean; + /** @example false */ + asBadge: boolean; + /** @example false */ + canEditMembersByModerator: boolean; + policies: { + pinLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canInvite: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + clipLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canHideAds: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + inviteLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + antennaLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + gtlAvailable: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + ltlAvailable: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + webhookLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canPublicNote: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + userListLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + wordMuteLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + alwaysMarkNsfw: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canSearchNotes: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + driveCapacityMb: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + rateLimitFactor: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + inviteLimitCycle: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + noteEachClipsLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + inviteExpirationTime: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canManageCustomEmojis: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + userEachUserListsLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canManageAvatarDecorations: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canUseTranslator: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + }; + usersCount: number; + }); + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +}; + +export type $defs = Record<string, never>; + +export type external = Record<string, never>; + +export type operations = { + + /** + * admin/meta + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/meta': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + cacheRemoteFiles: boolean; + cacheRemoteSensitiveFiles: boolean; + emailRequiredForSignup: boolean; + enableHcaptcha: boolean; + hcaptchaSiteKey: string | null; + enableRecaptcha: boolean; + recaptchaSiteKey: string | null; + enableTurnstile: boolean; + turnstileSiteKey: string | null; + swPublickey: string | null; + /** @default /assets/ai.png */ + mascotImageUrl: string | null; + bannerUrl: string | null; + serverErrorImageUrl: string | null; + infoImageUrl: string | null; + notFoundImageUrl: string | null; + iconUrl: string | null; + app192IconUrl: string | null; + app512IconUrl: string | null; + enableEmail: boolean; + enableServiceWorker: boolean; + translatorAvailable: boolean; + silencedHosts?: string[]; + pinnedUsers: string[]; + hiddenTags: string[]; + blockedHosts: string[]; + sensitiveWords: string[]; + preservedUsernames: string[]; + hcaptchaSecretKey: string | null; + recaptchaSecretKey: string | null; + turnstileSecretKey: string | null; + sensitiveMediaDetection: string; + sensitiveMediaDetectionSensitivity: string; + setSensitiveFlagAutomatically: boolean; + enableSensitiveMediaDetectionForVideos: boolean; + /** Format: id */ + proxyAccountId: string | null; + email: string | null; + smtpSecure: boolean; + smtpHost: string | null; + smtpPort: number | null; + smtpUser: string | null; + smtpPass: string | null; + swPrivateKey: string | null; + useObjectStorage: boolean; + objectStorageBaseUrl: string | null; + objectStorageBucket: string | null; + objectStoragePrefix: string | null; + objectStorageEndpoint: string | null; + objectStorageRegion: string | null; + objectStoragePort: number | null; + objectStorageAccessKey: string | null; + objectStorageSecretKey: string | null; + objectStorageUseSSL: boolean; + objectStorageUseProxy: boolean; + objectStorageSetPublicRead: boolean; + enableIpLogging: boolean; + enableActiveEmailValidation: boolean; + enableVerifymailApi: boolean; + verifymailAuthKey: string | null; + enableChartsForRemoteUser: boolean; + enableChartsForFederatedInstances: boolean; + enableServerMachineStats: boolean; + enableIdenticonGeneration: boolean; + manifestJsonOverride: string; + policies: Record<string, never>; + enableFanoutTimeline: boolean; + enableFanoutTimelineDbFallback: boolean; + perLocalUserUserTimelineCacheMax: number; + perRemoteUserUserTimelineCacheMax: number; + perUserHomeTimelineCacheMax: number; + perUserListTimelineCacheMax: number; + notesPerOneAd: number; + backgroundImageUrl: string | null; + deeplAuthKey: string | null; + deeplIsPro: boolean; + defaultDarkTheme: string | null; + defaultLightTheme: string | null; + description: string | null; + disableRegistration: boolean; + impressumUrl: string | null; + maintainerEmail: string | null; + maintainerName: string | null; + name: string | null; + objectStorageS3ForcePathStyle: boolean; + privacyPolicyUrl: string | null; + repositoryUrl: string; + summalyProxy: string | null; + themeColor: string | null; + tosUrl: string | null; + uri: string; + version: string; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/abuse-user-reports + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/abuse-user-reports': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default null */ + state?: string | null; + /** + * @default combined + * @enum {string} + */ + reporterOrigin?: 'combined' | 'local' | 'remote'; + /** + * @default combined + * @enum {string} + */ + targetUserOrigin?: 'combined' | 'local' | 'remote'; + /** @default false */ + forwarded?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': ({ + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + comment: string; + /** @example false */ + resolved: boolean; + /** Format: id */ + reporterId: string; + /** Format: id */ + targetUserId: string; + /** Format: id */ + assigneeId: string | null; + reporter: components['schemas']['User']; + targetUser: components['schemas']['User']; + assignee?: components['schemas']['User'] | null; + })[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/accounts/create + * @description No description provided. + * + * **Credential required**: *No* + */ + 'admin/accounts/create': { + requestBody: { + content: { + 'application/json': { + username: string; + password: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['User']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/accounts/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/accounts/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/accounts/find-by-email + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/accounts/find-by-email': { + requestBody: { + content: { + 'application/json': { + email: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/ad/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/ad/create': { + requestBody: { + content: { + 'application/json': { + url: string; + memo: string; + place: string; + priority: string; + ratio: number; + expiresAt: number; + startsAt: number; + imageUrl: string; + dayOfWeek: number; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/ad/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/ad/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + id: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/ad/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/ad/list': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default null */ + publishing?: boolean | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/ad/update + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/ad/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + id: string; + memo: string; + url: string; + imageUrl: string; + place: string; + priority: string; + ratio: number; + expiresAt: number; + startsAt: number; + dayOfWeek: number; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/announcements/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/announcements/create': { + requestBody: { + content: { + 'application/json': { + title: string; + text: string; + imageUrl: string | null; + /** + * @default info + * @enum {string} + */ + icon?: 'info' | 'warning' | 'error' | 'success'; + /** + * @default normal + * @enum {string} + */ + display?: 'normal' | 'banner' | 'dialog'; + /** @default false */ + forExistingUsers?: boolean; + /** @default false */ + silence?: boolean; + /** @default false */ + needConfirmationToRead?: boolean; + /** + * Format: misskey:id + * @default null + */ + userId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string | null; + title: string; + text: string; + imageUrl: string | null; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/announcements/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/announcements/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + id: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/announcements/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/announcements/list': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** Format: misskey:id */ + userId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': ({ + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string | null; + text: string; + title: string; + imageUrl: string | null; + reads: number; + })[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/announcements/update + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/announcements/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + id: string; + title?: string; + text?: string; + imageUrl?: string | null; + /** @enum {string} */ + icon?: 'info' | 'warning' | 'error' | 'success'; + /** @enum {string} */ + display?: 'normal' | 'banner' | 'dialog'; + forExistingUsers?: boolean; + silence?: boolean; + needConfirmationToRead?: boolean; + isActive?: boolean; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/avatar-decorations/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/avatar-decorations/create': { + requestBody: { + content: { + 'application/json': { + name: string; + description: string; + url: string; + roleIdsThatCanBeUsedThisDecoration?: string[]; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/avatar-decorations/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/avatar-decorations/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + id: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/avatar-decorations/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/avatar-decorations/list': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** Format: misskey:id */ + userId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': ({ + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string | null; + name: string; + description: string; + url: string; + roleIdsThatCanBeUsedThisDecoration: string[]; + })[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/avatar-decorations/update + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/avatar-decorations/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + id: string; + name?: string; + description?: string; + url?: string; + roleIdsThatCanBeUsedThisDecoration?: string[]; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/delete-all-files-of-a-user + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/delete-all-files-of-a-user': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/unset-user-avatar + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/unset-user-avatar': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/unset-user-banner + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/unset-user-banner': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/drive/clean-remote-files + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/drive/clean-remote-files': { + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/drive/cleanup + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/drive/cleanup': { + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/drive/files + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/drive/files': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** Format: misskey:id */ + userId?: string | null; + type?: string | null; + /** + * @default local + * @enum {string} + */ + origin?: 'combined' | 'local' | 'remote'; + /** + * @description The local host is represented with `null`. + * @default null + */ + hostname?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFile'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/drive/show-file + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/drive/show-file': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + fileId?: string; + url?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** + * Format: id + * @example xxxxxxxxxx + */ + userId: string | null; + /** @description The local host is represented with `null`. */ + userHost: string | null; + /** + * Format: md5 + * @example 15eca7fba0480996e2245f5185bf39f2 + */ + md5: string; + /** @example lenna.jpg */ + name: string; + /** @example image/jpeg */ + type: string; + /** @example 51469 */ + size: number; + comment: string | null; + blurhash: string | null; + properties: Record<string, never>; + /** @example true */ + storedInternal: boolean | null; + /** Format: url */ + url: string | null; + /** Format: url */ + thumbnailUrl: string | null; + /** Format: url */ + webpublicUrl: string | null; + accessKey: string | null; + thumbnailAccessKey: string | null; + webpublicAccessKey: string | null; + uri: string | null; + src: string | null; + /** + * Format: id + * @example xxxxxxxxxx + */ + folderId: string | null; + isSensitive: boolean; + isLink: boolean; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/add-aliases-bulk + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/emoji/add-aliases-bulk': { + requestBody: { + content: { + 'application/json': { + ids: string[]; + aliases: string[]; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/add + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/emoji/add': { + requestBody: { + content: { + 'application/json': { + name: string; + /** Format: misskey:id */ + fileId: string; + /** @description Use `null` to reset the category. */ + category?: string | null; + aliases?: string[]; + license?: string | null; + isSensitive?: boolean; + localOnly?: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/copy + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/emoji/copy': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + emojiId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** Format: id */ + id: string; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/delete-bulk + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/emoji/delete-bulk': { + requestBody: { + content: { + 'application/json': { + ids: string[]; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/emoji/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + id: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/import-zip + * @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* + */ + 'admin/emoji/import-zip': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + fileId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/list-remote + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/emoji/list-remote': { + requestBody: { + content: { + 'application/json': { + /** @default null */ + query?: string | null; + /** + * @description Use `null` to represent the local host. + * @default null + */ + host?: string | null; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': ({ + /** Format: id */ + id: string; + aliases: string[]; + name: string; + category: string | null; + /** @description The local host is represented with `null`. */ + host: string | null; + url: string; + })[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/emoji/list': { + requestBody: { + content: { + 'application/json': { + /** @default null */ + query?: string | null; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': ({ + /** Format: id */ + id: string; + aliases: string[]; + name: string; + category: string | null; + /** @description The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files. */ + host: string | null; + url: string; + })[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/remove-aliases-bulk + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/emoji/remove-aliases-bulk': { + requestBody: { + content: { + 'application/json': { + ids: string[]; + aliases: string[]; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/set-aliases-bulk + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/emoji/set-aliases-bulk': { + requestBody: { + content: { + 'application/json': { + ids: string[]; + aliases: string[]; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/set-category-bulk + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/emoji/set-category-bulk': { + requestBody: { + content: { + 'application/json': { + ids: string[]; + /** @description Use `null` to reset the category. */ + category?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/set-license-bulk + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/emoji/set-license-bulk': { + requestBody: { + content: { + 'application/json': { + ids: string[]; + /** @description Use `null` to reset the license. */ + license?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/emoji/update + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/emoji/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + id: string; + name: string; + /** Format: misskey:id */ + fileId?: string; + /** @description Use `null` to reset the category. */ + category?: string | null; + aliases: string[]; + license?: string | null; + isSensitive?: boolean; + localOnly?: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/federation/delete-all-files + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/federation/delete-all-files': { + requestBody: { + content: { + 'application/json': { + host: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/federation/refresh-remote-instance-metadata + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/federation/refresh-remote-instance-metadata': { + requestBody: { + content: { + 'application/json': { + host: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/federation/remove-all-following + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/federation/remove-all-following': { + requestBody: { + content: { + 'application/json': { + host: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/federation/update-instance + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/federation/update-instance': { + requestBody: { + content: { + 'application/json': { + host: string; + isSuspended: boolean; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/get-index-stats + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/get-index-stats': { + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/get-table-stats + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/get-table-stats': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': Record<string, 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/get-user-ips + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/get-user-ips': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/invite/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/invite/create': { + requestBody: { + content: { + 'application/json': { + /** @default 1 */ + count?: number; + expiresAt?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['InviteCode'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/invite/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/invite/list': { + requestBody: { + content: { + 'application/json': { + /** @default 30 */ + limit?: number; + /** @default 0 */ + offset?: number; + /** + * @default all + * @enum {string} + */ + type?: 'unused' | 'used' | 'expired' | 'all'; + /** @enum {string} */ + sort?: '+createdAt' | '-createdAt' | '+usedAt' | '-usedAt'; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['InviteCode'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/promo/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/promo/create': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + expiresAt: number; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/queue/clear + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/queue/clear': { + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/queue/deliver-delayed + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/queue/deliver-delayed': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': ((string | number)[])[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/queue/inbox-delayed + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/queue/inbox-delayed': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': ((string | number)[])[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/queue/promote + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/queue/promote': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + type: 'deliver' | 'inbox'; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/queue/stats + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/queue/stats': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + deliver: components['schemas']['QueueCount']; + inbox: components['schemas']['QueueCount']; + db: components['schemas']['QueueCount']; + objectStorage: components['schemas']['QueueCount']; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/relays/add + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/relays/add': { + requestBody: { + content: { + 'application/json': { + inbox: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** Format: id */ + id: string; + /** Format: url */ + inbox: string; + /** + * @default requesting + * @enum {string} + */ + status: 'requesting' | 'accepted' | 'rejected'; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/relays/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/relays/list': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': ({ + /** Format: id */ + id: string; + /** Format: url */ + inbox: string; + /** + * @default requesting + * @enum {string} + */ + status: 'requesting' | 'accepted' | 'rejected'; + })[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/relays/remove + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/relays/remove': { + requestBody: { + content: { + 'application/json': { + inbox: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/reset-password + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/reset-password': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + password: string; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/resolve-abuse-user-report + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/resolve-abuse-user-report': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + reportId: string; + /** @default false */ + forward?: boolean; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/send-email + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/send-email': { + requestBody: { + content: { + 'application/json': { + to: string; + subject: string; + text: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/server-info + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/server-info': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + machine: string; + /** @example linux */ + os: string; + node: string; + psql: string; + cpu: { + model: string; + cores: number; + }; + mem: { + /** Format: bytes */ + total: number; + }; + fs: { + /** Format: bytes */ + total: number; + /** Format: bytes */ + used: number; + }; + net: { + /** @example eth0 */ + interface: string; + }; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/show-moderation-logs + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/show-moderation-logs': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + type?: string | null; + /** Format: misskey:id */ + userId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + type: string; + info: Record<string, never>; + /** Format: id */ + userId: string; + user: components['schemas']['UserDetailed']; + }[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/show-user + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/show-user': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': Record<string, 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/show-users + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/show-users': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** @default 0 */ + offset?: number; + /** @enum {string} */ + sort?: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt' | '+lastActiveDate' | '-lastActiveDate'; + /** + * @default all + * @enum {string} + */ + state?: 'all' | 'alive' | 'available' | 'admin' | 'moderator' | 'adminOrModerator' | 'suspended'; + /** + * @default combined + * @enum {string} + */ + origin?: 'combined' | 'local' | 'remote'; + /** @default null */ + username?: string | null; + /** + * @description The local host is represented with `null`. + * @default null + */ + hostname?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserDetailed'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/suspend-user + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/suspend-user': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/unsuspend-user + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/unsuspend-user': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/update-meta + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/update-meta': { + requestBody: { + content: { + 'application/json': { + disableRegistration?: boolean | null; + pinnedUsers?: string[] | null; + hiddenTags?: string[] | null; + blockedHosts?: string[] | null; + sensitiveWords?: string[] | null; + themeColor?: string | null; + mascotImageUrl?: string | null; + bannerUrl?: string | null; + serverErrorImageUrl?: string | null; + infoImageUrl?: string | null; + notFoundImageUrl?: string | null; + iconUrl?: string | null; + app192IconUrl?: string | null; + app512IconUrl?: string | null; + backgroundImageUrl?: string | null; + logoImageUrl?: string | null; + name?: string | null; + shortName?: string | null; + description?: string | null; + defaultLightTheme?: string | null; + defaultDarkTheme?: string | null; + cacheRemoteFiles?: boolean; + cacheRemoteSensitiveFiles?: boolean; + emailRequiredForSignup?: boolean; + enableHcaptcha?: boolean; + hcaptchaSiteKey?: string | null; + hcaptchaSecretKey?: string | null; + enableRecaptcha?: boolean; + recaptchaSiteKey?: string | null; + recaptchaSecretKey?: string | null; + enableTurnstile?: boolean; + turnstileSiteKey?: string | null; + turnstileSecretKey?: string | null; + /** @enum {string} */ + sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote'; + /** @enum {string} */ + sensitiveMediaDetectionSensitivity?: 'medium' | 'low' | 'high' | 'veryLow' | 'veryHigh'; + setSensitiveFlagAutomatically?: boolean; + enableSensitiveMediaDetectionForVideos?: boolean; + /** Format: misskey:id */ + proxyAccountId?: string | null; + maintainerName?: string | null; + maintainerEmail?: string | null; + langs?: string[]; + summalyProxy?: string | null; + deeplAuthKey?: string | null; + deeplIsPro?: boolean; + enableEmail?: boolean; + email?: string | null; + smtpSecure?: boolean; + smtpHost?: string | null; + smtpPort?: number | null; + smtpUser?: string | null; + smtpPass?: string | null; + enableServiceWorker?: boolean; + swPublicKey?: string | null; + swPrivateKey?: string | null; + tosUrl?: string | null; + repositoryUrl?: string; + feedbackUrl?: string; + impressumUrl?: string | null; + privacyPolicyUrl?: string | null; + useObjectStorage?: boolean; + objectStorageBaseUrl?: string | null; + objectStorageBucket?: string | null; + objectStoragePrefix?: string | null; + objectStorageEndpoint?: string | null; + objectStorageRegion?: string | null; + objectStoragePort?: number | null; + objectStorageAccessKey?: string | null; + objectStorageSecretKey?: string | null; + objectStorageUseSSL?: boolean; + objectStorageUseProxy?: boolean; + objectStorageSetPublicRead?: boolean; + objectStorageS3ForcePathStyle?: boolean; + enableIpLogging?: boolean; + enableActiveEmailValidation?: boolean; + enableVerifymailApi?: boolean; + verifymailAuthKey?: string | null; + enableChartsForRemoteUser?: boolean; + enableChartsForFederatedInstances?: boolean; + enableServerMachineStats?: boolean; + enableIdenticonGeneration?: boolean; + serverRules?: string[]; + preservedUsernames?: string[]; + manifestJsonOverride?: string; + enableFanoutTimeline?: boolean; + enableFanoutTimelineDbFallback?: boolean; + perLocalUserUserTimelineCacheMax?: number; + perRemoteUserUserTimelineCacheMax?: number; + perUserHomeTimelineCacheMax?: number; + perUserListTimelineCacheMax?: number; + notesPerOneAd?: number; + silencedHosts?: string[] | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/delete-account + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/delete-account': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': unknown; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/update-user-note + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/update-user-note': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + text: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/roles/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/roles/create': { + requestBody: { + content: { + 'application/json': { + name: string; + description: string; + color: string | null; + iconUrl: string | null; + /** @enum {string} */ + target: 'manual' | 'conditional'; + condFormula: Record<string, never>; + isPublic: boolean; + isModerator: boolean; + isAdministrator: boolean; + /** @default false */ + isExplorable?: boolean; + asBadge: boolean; + canEditMembersByModerator: boolean; + displayOrder: number; + policies: Record<string, never>; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Role']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/roles/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/roles/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roleId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/roles/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/roles/list': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Role'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/roles/show + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/roles/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roleId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Role']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/roles/update + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/roles/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roleId: string; + name: string; + description: string; + color: string | null; + iconUrl: string | null; + /** @enum {string} */ + target: 'manual' | 'conditional'; + condFormula: Record<string, never>; + isPublic: boolean; + isModerator: boolean; + isAdministrator: boolean; + isExplorable?: boolean; + asBadge: boolean; + canEditMembersByModerator: boolean; + displayOrder: number; + policies: Record<string, never>; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/roles/assign + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/roles/assign': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roleId: string; + /** Format: misskey:id */ + userId: string; + expiresAt?: number | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/roles/unassign + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/roles/unassign': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roleId: string; + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/roles/update-default-policies + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'admin/roles/update-default-policies': { + requestBody: { + content: { + 'application/json': { + policies: Record<string, never>; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/roles/users + * @description No description provided. + * + * **Credential required**: *No* + */ + 'admin/roles/users': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roleId: string; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * announcements + * @description No description provided. + * + * **Credential required**: *No* + */ + announcements: { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default true */ + isActive?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Announcement'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * antennas/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'antennas/create': { + requestBody: { + content: { + 'application/json': { + name: string; + /** @enum {string} */ + src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist'; + /** Format: misskey:id */ + userListId?: string | null; + keywords: string[][]; + excludeKeywords: string[][]; + users: string[]; + caseSensitive: boolean; + localOnly?: boolean; + withReplies: boolean; + withFile: boolean; + notify: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Antenna']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * antennas/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'antennas/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + antennaId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * antennas/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + 'antennas/list': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Antenna'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * antennas/notes + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + 'antennas/notes': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + antennaId: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * antennas/show + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + 'antennas/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + antennaId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Antenna']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * antennas/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'antennas/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + antennaId: string; + name: string; + /** @enum {string} */ + src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist'; + /** Format: misskey:id */ + userListId?: string | null; + keywords: string[][]; + excludeKeywords: string[][]; + users: string[]; + caseSensitive: boolean; + localOnly?: boolean; + withReplies: boolean; + withFile: boolean; + notify: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Antenna']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * ap/get + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'ap/get': { + requestBody: { + content: { + 'application/json': { + uri: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': Record<string, 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']; + }; + }; + }; + }; + /** + * ap/show + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'ap/show': { + requestBody: { + content: { + 'application/json': { + uri: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': OneOf<[{ + /** @enum {string} */ + type: 'User'; + object: components['schemas']['UserDetailedNotMe']; + }, { + /** @enum {string} */ + type: 'Note'; + object: components['schemas']['Note']; + }]>; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * app/create + * @description No description provided. + * + * **Credential required**: *No* + */ + 'app/create': { + requestBody: { + content: { + 'application/json': { + name: string; + description: string; + permission: string[]; + callbackUrl?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['App']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * app/show + * @description No description provided. + * + * **Credential required**: *No* + */ + 'app/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + appId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['App']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * auth/accept + * @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* + */ + 'auth/accept': { + requestBody: { + content: { + 'application/json': { + token: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * auth/session/generate + * @description No description provided. + * + * **Credential required**: *No* + */ + 'auth/session/generate': { + requestBody: { + content: { + 'application/json': { + appSecret: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + token: string; + /** Format: url */ + url: string; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * auth/session/show + * @description No description provided. + * + * **Credential required**: *No* + */ + 'auth/session/show': { + requestBody: { + content: { + 'application/json': { + token: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** Format: id */ + id: string; + app: components['schemas']['App']; + token: string; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * auth/session/userkey + * @description No description provided. + * + * **Credential required**: *No* + */ + 'auth/session/userkey': { + requestBody: { + content: { + 'application/json': { + appSecret: string; + token: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + accessToken: string; + user: components['schemas']['UserDetailedNotMe']; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * blocking/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:blocks* + */ + 'blocking/create': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserDetailedNotMe']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * blocking/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:blocks* + */ + 'blocking/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserDetailedNotMe']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * blocking/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:blocks* + */ + 'blocking/list': { + requestBody: { + content: { + 'application/json': { + /** @default 30 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Blocking'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * channels/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + 'channels/create': { + requestBody: { + content: { + 'application/json': { + name: string; + description?: string | null; + /** Format: misskey:id */ + bannerId?: string | null; + color?: string; + isSensitive?: boolean | null; + allowRenoteToExternal?: boolean | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Channel']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * channels/featured + * @description No description provided. + * + * **Credential required**: *No* + */ + 'channels/featured': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Channel'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * channels/follow + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + 'channels/follow': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + channelId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * channels/followed + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:channels* + */ + 'channels/followed': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 5 */ + limit?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Channel'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * channels/owned + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:channels* + */ + 'channels/owned': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 5 */ + limit?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Channel'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * channels/show + * @description No description provided. + * + * **Credential required**: *No* + */ + 'channels/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + channelId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Channel']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * channels/timeline + * @description No description provided. + * + * **Credential required**: *No* + */ + 'channels/timeline': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + channelId: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + /** @default false */ + allowPartial?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * channels/unfollow + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + 'channels/unfollow': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + channelId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * channels/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + 'channels/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + channelId: string; + name?: string; + description?: string | null; + /** Format: misskey:id */ + bannerId?: string | null; + isArchived?: boolean | null; + pinnedNoteIds?: string[]; + color?: string; + isSensitive?: boolean | null; + allowRenoteToExternal?: boolean | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Channel']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * channels/favorite + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + 'channels/favorite': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + channelId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * channels/unfavorite + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:channels* + */ + 'channels/unfavorite': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + channelId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * channels/my-favorites + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:channels* + */ + 'channels/my-favorites': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Channel'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * channels/search + * @description No description provided. + * + * **Credential required**: *No* + */ + 'channels/search': { + requestBody: { + content: { + 'application/json': { + query: string; + /** + * @default nameAndDescription + * @enum {string} + */ + type?: 'nameAndDescription' | 'nameOnly'; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 5 */ + limit?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Channel'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * charts/active-users + * @description No description provided. + * + * **Credential required**: *No* + */ + 'charts/active-users': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + span: 'day' | 'hour'; + /** @default 30 */ + limit?: number; + /** @default null */ + offset?: number | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + readWrite: number[]; + read: number[]; + write: number[]; + registeredWithinWeek: number[]; + registeredWithinMonth: number[]; + registeredWithinYear: number[]; + registeredOutsideWeek: number[]; + registeredOutsideMonth: number[]; + registeredOutsideYear: number[]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * charts/ap-request + * @description No description provided. + * + * **Credential required**: *No* + */ + 'charts/ap-request': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + span: 'day' | 'hour'; + /** @default 30 */ + limit?: number; + /** @default null */ + offset?: number | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + deliverFailed: number[]; + deliverSucceeded: number[]; + inboxReceived: number[]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * charts/drive + * @description No description provided. + * + * **Credential required**: *No* + */ + 'charts/drive': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + span: 'day' | 'hour'; + /** @default 30 */ + limit?: number; + /** @default null */ + offset?: number | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + 'local.incCount': number[]; + 'local.incSize': number[]; + 'local.decCount': number[]; + 'local.decSize': number[]; + 'remote.incCount': number[]; + 'remote.incSize': number[]; + 'remote.decCount': number[]; + 'remote.decSize': number[]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * charts/federation + * @description No description provided. + * + * **Credential required**: *No* + */ + 'charts/federation': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + span: 'day' | 'hour'; + /** @default 30 */ + limit?: number; + /** @default null */ + offset?: number | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + deliveredInstances: number[]; + inboxInstances: number[]; + stalled: number[]; + sub: number[]; + pub: number[]; + pubsub: number[]; + subActive: number[]; + pubActive: number[]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * charts/instance + * @description No description provided. + * + * **Credential required**: *No* + */ + 'charts/instance': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + span: 'day' | 'hour'; + /** @default 30 */ + limit?: number; + /** @default null */ + offset?: number | null; + host: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + 'requests.failed': number[]; + 'requests.succeeded': number[]; + 'requests.received': number[]; + 'notes.total': number[]; + 'notes.inc': number[]; + 'notes.dec': number[]; + 'notes.diffs.normal': number[]; + 'notes.diffs.reply': number[]; + 'notes.diffs.renote': number[]; + 'notes.diffs.withFile': number[]; + 'users.total': number[]; + 'users.inc': number[]; + 'users.dec': number[]; + 'following.total': number[]; + 'following.inc': number[]; + 'following.dec': number[]; + 'followers.total': number[]; + 'followers.inc': number[]; + 'followers.dec': number[]; + 'drive.totalFiles': number[]; + 'drive.incFiles': number[]; + 'drive.decFiles': number[]; + 'drive.incUsage': number[]; + 'drive.decUsage': number[]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * charts/notes + * @description No description provided. + * + * **Credential required**: *No* + */ + 'charts/notes': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + span: 'day' | 'hour'; + /** @default 30 */ + limit?: number; + /** @default null */ + offset?: number | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + 'local.total': number[]; + 'local.inc': number[]; + 'local.dec': number[]; + 'local.diffs.normal': number[]; + 'local.diffs.reply': number[]; + 'local.diffs.renote': number[]; + 'local.diffs.withFile': number[]; + 'remote.total': number[]; + 'remote.inc': number[]; + 'remote.dec': number[]; + 'remote.diffs.normal': number[]; + 'remote.diffs.reply': number[]; + 'remote.diffs.renote': number[]; + 'remote.diffs.withFile': number[]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * charts/user/drive + * @description No description provided. + * + * **Credential required**: *No* + */ + 'charts/user/drive': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + span: 'day' | 'hour'; + /** @default 30 */ + limit?: number; + /** @default null */ + offset?: number | null; + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + totalCount: number[]; + totalSize: number[]; + incCount: number[]; + incSize: number[]; + decCount: number[]; + decSize: number[]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * charts/user/following + * @description No description provided. + * + * **Credential required**: *No* + */ + 'charts/user/following': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + span: 'day' | 'hour'; + /** @default 30 */ + limit?: number; + /** @default null */ + offset?: number | null; + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + 'local.followings.total': number[]; + 'local.followings.inc': number[]; + 'local.followings.dec': number[]; + 'local.followers.total': number[]; + 'local.followers.inc': number[]; + 'local.followers.dec': number[]; + 'remote.followings.total': number[]; + 'remote.followings.inc': number[]; + 'remote.followings.dec': number[]; + 'remote.followers.total': number[]; + 'remote.followers.inc': number[]; + 'remote.followers.dec': number[]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * charts/user/notes + * @description No description provided. + * + * **Credential required**: *No* + */ + 'charts/user/notes': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + span: 'day' | 'hour'; + /** @default 30 */ + limit?: number; + /** @default null */ + offset?: number | null; + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + total: number[]; + inc: number[]; + dec: number[]; + 'diffs.normal': number[]; + 'diffs.reply': number[]; + 'diffs.renote': number[]; + 'diffs.withFile': number[]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * charts/user/pv + * @description No description provided. + * + * **Credential required**: *No* + */ + 'charts/user/pv': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + span: 'day' | 'hour'; + /** @default 30 */ + limit?: number; + /** @default null */ + offset?: number | null; + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + 'upv.user': number[]; + 'pv.user': number[]; + 'upv.visitor': number[]; + 'pv.visitor': number[]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * charts/user/reactions + * @description No description provided. + * + * **Credential required**: *No* + */ + 'charts/user/reactions': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + span: 'day' | 'hour'; + /** @default 30 */ + limit?: number; + /** @default null */ + offset?: number | null; + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + 'local.count': number[]; + 'remote.count': number[]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * charts/users + * @description No description provided. + * + * **Credential required**: *No* + */ + 'charts/users': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + span: 'day' | 'hour'; + /** @default 30 */ + limit?: number; + /** @default null */ + offset?: number | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + 'local.total': number[]; + 'local.inc': number[]; + 'local.dec': number[]; + 'remote.total': number[]; + 'remote.inc': number[]; + 'remote.dec': number[]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * clips/add-note + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'clips/add-note': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + clipId: string; + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * clips/remove-note + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'clips/remove-note': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + clipId: string; + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * clips/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'clips/create': { + requestBody: { + content: { + 'application/json': { + name: string; + /** @default false */ + isPublic?: boolean; + description?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Clip']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * clips/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'clips/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + clipId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * clips/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + 'clips/list': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Clip'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * clips/notes + * @description No description provided. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + 'clips/notes': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + clipId: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * clips/show + * @description No description provided. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + 'clips/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + clipId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Clip']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * clips/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'clips/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + clipId: string; + name: string; + isPublic?: boolean; + description?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Clip']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * clips/favorite + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:clip-favorite* + */ + 'clips/favorite': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + clipId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * clips/unfavorite + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:clip-favorite* + */ + 'clips/unfavorite': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + clipId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * clips/my-favorites + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:clip-favorite* + */ + 'clips/my-favorites': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Clip'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + drive: { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + capacity: number; + usage: number; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/files + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + 'drive/files': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** + * Format: misskey:id + * @default null + */ + folderId?: string | null; + type?: string | null; + /** @enum {string|null} */ + sort?: '+createdAt' | '-createdAt' | '+name' | '-name' | '+size' | '-size' | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFile'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/files/attached-notes + * @description Find the notes to which the given file is attached. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + 'drive/files/attached-notes': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + fileId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/files/check-existence + * @description Check if a given file exists. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + 'drive/files/check-existence': { + requestBody: { + content: { + 'application/json': { + md5: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': boolean; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/files/create + * @description Upload a new drive file. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + 'drive/files/create': { + requestBody: { + content: { + 'multipart/form-data': { + /** + * Format: misskey:id + * @default null + */ + folderId?: string | null; + /** @default null */ + name?: string | null; + /** @default null */ + comment?: string | null; + /** @default false */ + isSensitive?: boolean; + /** @default false */ + force?: boolean; + /** + * Format: binary + * @description The file contents. + */ + file: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFile']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * drive/files/delete + * @description Delete an existing drive file. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + 'drive/files/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + fileId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/files/find-by-hash + * @description Search for a drive file by a hash of the contents. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + 'drive/files/find-by-hash': { + requestBody: { + content: { + 'application/json': { + md5: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFile'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/files/find + * @description Search for a drive file by the given parameters. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + 'drive/files/find': { + requestBody: { + content: { + 'application/json': { + name: string; + /** + * Format: misskey:id + * @default null + */ + folderId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFile'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/files/show + * @description Show the properties of a drive file. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + 'drive/files/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + fileId?: string; + url?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFile']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/files/update + * @description Update the properties of a drive file. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + 'drive/files/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + fileId: string; + /** Format: misskey:id */ + folderId?: string | null; + name?: string; + isSensitive?: boolean; + comment?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFile']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/files/upload-from-url + * @description Request the server to download a new drive file from the specified URL. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + 'drive/files/upload-from-url': { + requestBody: { + content: { + 'application/json': { + url: string; + /** + * Format: misskey:id + * @default null + */ + folderId?: string | null; + /** @default false */ + isSensitive?: boolean; + /** @default null */ + comment?: string | null; + /** @default null */ + marker?: string | null; + /** @default false */ + force?: boolean; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * drive/folders + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + 'drive/folders': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** + * Format: misskey:id + * @default null + */ + folderId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFolder'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/folders/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + 'drive/folders/create': { + requestBody: { + content: { + 'application/json': { + /** @default Untitled */ + name?: string; + /** Format: misskey:id */ + parentId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFolder']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * drive/folders/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + 'drive/folders/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + folderId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/folders/find + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + 'drive/folders/find': { + requestBody: { + content: { + 'application/json': { + name: string; + /** + * Format: misskey:id + * @default null + */ + parentId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFolder'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/folders/show + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + 'drive/folders/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + folderId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFolder']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/folders/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:drive* + */ + 'drive/folders/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + folderId: string; + name?: string; + /** Format: misskey:id */ + parentId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFolder']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * drive/stream + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + 'drive/stream': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + type?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['DriveFile'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * email-address/available + * @description No description provided. + * + * **Credential required**: *No* + */ + 'email-address/available': { + requestBody: { + content: { + 'application/json': { + emailAddress: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + available: boolean; + reason: string | null; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * endpoint + * @description No description provided. + * + * **Credential required**: *No* + */ + endpoint: { + requestBody: { + content: { + 'application/json': { + endpoint: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * endpoints + * @description No description provided. + * + * **Credential required**: *No* + */ + endpoints: { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': string[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * export-custom-emojis + * @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* + */ + 'export-custom-emojis': { + 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']; + }; + }; + }; + }; + /** + * federation/followers + * @description No description provided. + * + * **Credential required**: *No* + */ + 'federation/followers': { + requestBody: { + content: { + 'application/json': { + host: string; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Following'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * federation/following + * @description No description provided. + * + * **Credential required**: *No* + */ + 'federation/following': { + requestBody: { + content: { + 'application/json': { + host: string; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Following'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * federation/instances + * @description No description provided. + * + * **Credential required**: *No* + */ + 'federation/instances': { + requestBody: { + content: { + 'application/json': { + /** @description Omit or use `null` to not filter by host. */ + host?: string | null; + blocked?: boolean | null; + notResponding?: boolean | null; + suspended?: boolean | null; + silenced?: boolean | null; + federating?: boolean | null; + subscribing?: boolean | null; + publishing?: boolean | null; + /** @default 30 */ + limit?: number; + /** @default 0 */ + offset?: number; + /** @enum {string|null} */ + sort?: '+pubSub' | '-pubSub' | '+notes' | '-notes' | '+users' | '-users' | '+following' | '-following' | '+followers' | '-followers' | '+firstRetrievedAt' | '-firstRetrievedAt' | '+latestRequestReceivedAt' | '-latestRequestReceivedAt' | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['FederationInstance'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * federation/show-instance + * @description No description provided. + * + * **Credential required**: *No* + */ + 'federation/show-instance': { + requestBody: { + content: { + 'application/json': { + host: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['FederationInstance'] | null; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * federation/update-remote-user + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'federation/update-remote-user': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * federation/users + * @description No description provided. + * + * **Credential required**: *No* + */ + 'federation/users': { + requestBody: { + content: { + 'application/json': { + host: string; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserDetailedNotMe'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * federation/stats + * @description No description provided. + * + * **Credential required**: *No* + */ + 'federation/stats': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * following/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + 'following/create': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + withReplies?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserLite']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * following/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + 'following/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserLite']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * following/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + 'following/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + /** @enum {string} */ + notify?: 'normal' | 'none'; + withReplies?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserLite']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * following/update-all + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + 'following/update-all': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + notify?: 'normal' | 'none'; + withReplies?: boolean; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * following/invalidate + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + 'following/invalidate': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserLite']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * following/requests/accept + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + 'following/requests/accept': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * following/requests/cancel + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + 'following/requests/cancel': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserLite']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * following/requests/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:following* + */ + 'following/requests/list': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** Format: id */ + id: string; + follower: components['schemas']['UserLite']; + followee: components['schemas']['UserLite']; + }[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * following/requests/reject + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:following* + */ + 'following/requests/reject': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * gallery/featured + * @description No description provided. + * + * **Credential required**: *No* + */ + 'gallery/featured': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['GalleryPost'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * gallery/popular + * @description No description provided. + * + * **Credential required**: *No* + */ + 'gallery/popular': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['GalleryPost'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * gallery/posts + * @description No description provided. + * + * **Credential required**: *No* + */ + 'gallery/posts': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['GalleryPost'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * gallery/posts/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery* + */ + 'gallery/posts/create': { + requestBody: { + content: { + 'application/json': { + title: string; + description?: string | null; + fileIds: string[]; + /** @default false */ + isSensitive?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['GalleryPost']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * gallery/posts/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery* + */ + 'gallery/posts/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + postId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * gallery/posts/like + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery-likes* + */ + 'gallery/posts/like': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + postId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * gallery/posts/show + * @description No description provided. + * + * **Credential required**: *No* + */ + 'gallery/posts/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + postId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['GalleryPost']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * gallery/posts/unlike + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery-likes* + */ + 'gallery/posts/unlike': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + postId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * gallery/posts/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:gallery* + */ + 'gallery/posts/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + postId: string; + title: string; + description?: string | null; + fileIds: string[]; + /** @default false */ + isSensitive?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['GalleryPost']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * get-online-users-count + * @description No description provided. + * + * **Credential required**: *No* + */ + 'get-online-users-count': { + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * get-avatar-decorations + * @description No description provided. + * + * **Credential required**: *No* + */ + 'get-avatar-decorations': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + name: string; + description: string; + url: string; + roleIdsThatCanBeUsedThisDecoration: string[]; + }[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * hashtags/list + * @description No description provided. + * + * **Credential required**: *No* + */ + 'hashtags/list': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** @default false */ + attachedToUserOnly?: boolean; + /** @default false */ + attachedToLocalUserOnly?: boolean; + /** @default false */ + attachedToRemoteUserOnly?: boolean; + /** @enum {string} */ + sort: '+mentionedUsers' | '-mentionedUsers' | '+mentionedLocalUsers' | '-mentionedLocalUsers' | '+mentionedRemoteUsers' | '-mentionedRemoteUsers' | '+attachedUsers' | '-attachedUsers' | '+attachedLocalUsers' | '-attachedLocalUsers' | '+attachedRemoteUsers' | '-attachedRemoteUsers'; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Hashtag'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * hashtags/search + * @description No description provided. + * + * **Credential required**: *No* + */ + 'hashtags/search': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + query: string; + /** @default 0 */ + offset?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': string[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * hashtags/show + * @description No description provided. + * + * **Credential required**: *No* + */ + 'hashtags/show': { + requestBody: { + content: { + 'application/json': { + tag: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Hashtag']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * hashtags/trend + * @description No description provided. + * + * **Credential required**: *No* + */ + 'hashtags/trend': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + tag: string; + chart: number[]; + usersCount: number; + }[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * hashtags/users + * @description No description provided. + * + * **Credential required**: *No* + */ + 'hashtags/users': { + requestBody: { + content: { + 'application/json': { + tag: string; + /** @default 10 */ + limit?: number; + /** @enum {string} */ + sort: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt'; + /** + * @default all + * @enum {string} + */ + state?: 'all' | 'alive'; + /** + * @default local + * @enum {string} + */ + origin?: 'combined' | 'local' | 'remote'; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserDetailed'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i + * @description No description provided. + * + * **Credential required**: *Yes* + */ + i: { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['MeDetailed']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/2fa/done + * @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/2fa/done': { + requestBody: { + content: { + 'application/json': { + token: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/2fa/key-done + * @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/2fa/key-done': { + requestBody: { + content: { + 'application/json': { + password: string; + token?: string | null; + name: string; + credential: Record<string, never>; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/2fa/password-less + * @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/2fa/password-less': { + requestBody: { + content: { + 'application/json': { + value: boolean; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/2fa/register-key + * @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/2fa/register-key': { + requestBody: { + content: { + 'application/json': { + password: string; + token?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/2fa/register + * @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/2fa/register': { + requestBody: { + content: { + 'application/json': { + password: string; + token?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/2fa/update-key + * @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/2fa/update-key': { + requestBody: { + content: { + 'application/json': { + name: string; + credentialId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/2fa/remove-key + * @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/2fa/remove-key': { + requestBody: { + content: { + 'application/json': { + password: string; + token?: string | null; + credentialId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/2fa/unregister + * @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/2fa/unregister': { + requestBody: { + content: { + 'application/json': { + password: string; + token?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/apps + * @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/apps': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + sort?: '+createdAt' | '-createdAt' | '+lastUsedAt' | '-lastUsedAt'; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/authorized-apps + * @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/authorized-apps': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** @default 0 */ + offset?: number; + /** + * @default desc + * @enum {string} + */ + sort?: 'desc' | 'asc'; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/claim-achievement + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'i/claim-achievement': { + requestBody: { + 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'; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/change-password + * @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/change-password': { + requestBody: { + content: { + 'application/json': { + currentPassword: string; + newPassword: string; + token?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/delete-account + * @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/delete-account': { + requestBody: { + content: { + 'application/json': { + password: string; + token?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/export-blocking + * @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-blocking': { + 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-following + * @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-following': { + requestBody: { + content: { + 'application/json': { + /** @default false */ + excludeMuting?: boolean; + /** @default false */ + excludeInactive?: boolean; + }; + }; + }; + 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-mute + * @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-mute': { + 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-notes + * @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-notes': { + 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. + * + * **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-favorites': { + 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-user-lists + * @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-user-lists': { + 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-antennas + * @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-antennas': { + 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/favorites + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:favorites* + */ + 'i/favorites': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['NoteFavorite'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/gallery/likes + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:gallery-likes* + */ + 'i/gallery/likes': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** Format: id */ + id: string; + post: components['schemas']['GalleryPost']; + }[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/gallery/posts + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:gallery* + */ + 'i/gallery/posts': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['GalleryPost'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/import-blocking + * @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/import-blocking': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + fileId: string; + }; + }; + }; + 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/import-following + * @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/import-following': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + fileId: string; + withReplies?: boolean; + }; + }; + }; + 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/import-muting + * @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/import-muting': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + fileId: string; + }; + }; + }; + 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/import-user-lists + * @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/import-user-lists': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + fileId: string; + }; + }; + }; + 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/import-antennas + * @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/import-antennas': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + fileId: string; + }; + }; + }; + 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/notifications + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:notifications* + */ + 'i/notifications': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default true */ + markAsRead?: boolean; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Notification'][]; + }; + }; + /** @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/notifications-grouped + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:notifications* + */ + 'i/notifications-grouped': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default true */ + markAsRead?: boolean; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Notification'][]; + }; + }; + /** @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/page-likes + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:page-likes* + */ + 'i/page-likes': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** Format: id */ + id: string; + page: components['schemas']['Page']; + }[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/pages + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:pages* + */ + 'i/pages': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Page'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/pin + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'i/pin': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['MeDetailed']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/read-all-unread-notes + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'i/read-all-unread-notes': { + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/read-announcement + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'i/read-announcement': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + announcementId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/regenerate-token + * @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/regenerate-token': { + requestBody: { + content: { + 'application/json': { + password: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/registry/get-all + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'i/registry/get-all': { + requestBody: { + content: { + 'application/json': { + /** @default [] */ + scope: string[]; + domain?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/registry/get-detail + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'i/registry/get-detail': { + requestBody: { + content: { + 'application/json': { + key: string; + /** @default [] */ + scope: string[]; + domain?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/registry/get + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'i/registry/get': { + requestBody: { + content: { + 'application/json': { + key: string; + /** @default [] */ + scope: string[]; + domain?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/registry/keys-with-type + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'i/registry/keys-with-type': { + requestBody: { + content: { + 'application/json': { + /** @default [] */ + scope: string[]; + domain?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/registry/keys + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'i/registry/keys': { + requestBody: { + content: { + 'application/json': { + /** @default [] */ + scope: string[]; + domain?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/registry/remove + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'i/registry/remove': { + requestBody: { + content: { + 'application/json': { + key: string; + /** @default [] */ + scope: string[]; + domain?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/registry/scopes-with-domain + * @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/registry/scopes-with-domain': { + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/registry/set + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'i/registry/set': { + requestBody: { + content: { + 'application/json': { + key: string; + value: unknown; + /** @default [] */ + scope: string[]; + domain?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/revoke-token + * @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/revoke-token': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + tokenId?: string; + token?: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/signin-history + * @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/signin-history': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Signin'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/unpin + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'i/unpin': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['MeDetailed']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/update-email + * @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/update-email': { + requestBody: { + content: { + 'application/json': { + password: string; + email?: string | null; + token?: string | null; + }; + }; + }; + 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/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'i/update': { + requestBody: { + content: { + 'application/json': { + name?: string | null; + description?: string | null; + location?: string | null; + birthday?: string | null; + /** @enum {string|null} */ + lang?: null | 'ach' | 'ady' | 'af' | 'af-NA' | 'af-ZA' | 'ak' | 'ar' | 'ar-AR' | 'ar-MA' | 'ar-SA' | 'ay-BO' | 'az' | 'az-AZ' | 'be-BY' | 'bg' | 'bg-BG' | 'bn' | 'bn-IN' | 'bn-BD' | 'br' | 'bs-BA' | 'ca' | 'ca-ES' | 'cak' | 'ck-US' | 'cs' | 'cs-CZ' | 'cy' | 'cy-GB' | 'da' | 'da-DK' | 'de' | 'de-AT' | 'de-DE' | 'de-CH' | 'dsb' | 'el' | 'el-GR' | 'en' | 'en-GB' | 'en-AU' | 'en-CA' | 'en-IE' | 'en-IN' | 'en-PI' | 'en-SG' | 'en-UD' | 'en-US' | 'en-ZA' | 'en@pirate' | 'eo' | 'eo-EO' | 'es' | 'es-AR' | 'es-419' | 'es-CL' | 'es-CO' | 'es-EC' | 'es-ES' | 'es-LA' | 'es-NI' | 'es-MX' | 'es-US' | 'es-VE' | 'et' | 'et-EE' | 'eu' | 'eu-ES' | 'fa' | 'fa-IR' | 'fb-LT' | 'ff' | 'fi' | 'fi-FI' | 'fo' | 'fo-FO' | 'fr' | 'fr-CA' | 'fr-FR' | 'fr-BE' | 'fr-CH' | 'fy-NL' | 'ga' | 'ga-IE' | 'gd' | 'gl' | 'gl-ES' | 'gn-PY' | 'gu-IN' | 'gv' | 'gx-GR' | 'he' | 'he-IL' | 'hi' | 'hi-IN' | 'hr' | 'hr-HR' | 'hsb' | 'ht' | 'hu' | 'hu-HU' | 'hy' | 'hy-AM' | 'id' | 'id-ID' | 'is' | 'is-IS' | 'it' | 'it-IT' | 'ja' | 'ja-JP' | 'jv-ID' | 'ka-GE' | 'kk-KZ' | 'km' | 'kl' | 'km-KH' | 'kab' | 'kn' | 'kn-IN' | 'ko' | 'ko-KR' | 'ku-TR' | 'kw' | 'la' | 'la-VA' | 'lb' | 'li-NL' | 'lt' | 'lt-LT' | 'lv' | 'lv-LV' | 'mai' | 'mg-MG' | 'mk' | 'mk-MK' | 'ml' | 'ml-IN' | 'mn-MN' | 'mr' | 'mr-IN' | 'ms' | 'ms-MY' | 'mt' | 'mt-MT' | 'my' | 'no' | 'nb' | 'nb-NO' | 'ne' | 'ne-NP' | 'nl' | 'nl-BE' | 'nl-NL' | 'nn-NO' | 'oc' | 'or-IN' | 'pa' | 'pa-IN' | 'pl' | 'pl-PL' | 'ps-AF' | 'pt' | 'pt-BR' | 'pt-PT' | 'qu-PE' | 'rm-CH' | 'ro' | 'ro-RO' | 'ru' | 'ru-RU' | 'sa-IN' | 'se-NO' | 'sh' | 'si-LK' | 'sk' | 'sk-SK' | 'sl' | 'sl-SI' | 'so-SO' | 'sq' | 'sq-AL' | 'sr' | 'sr-RS' | 'su' | 'sv' | 'sv-SE' | 'sw' | 'sw-KE' | 'ta' | 'ta-IN' | 'te' | 'te-IN' | 'tg' | 'tg-TJ' | 'th' | 'th-TH' | 'fil' | 'tlh' | 'tr' | 'tr-TR' | 'tt-RU' | 'uk' | 'uk-UA' | 'ur' | 'ur-PK' | 'uz' | 'uz-UZ' | 'vi' | 'vi-VN' | 'xh-ZA' | 'yi' | 'yi-DE' | 'zh' | 'zh-Hans' | 'zh-Hant' | 'zh-CN' | 'zh-HK' | 'zh-SG' | 'zh-TW' | 'zu-ZA'; + /** Format: misskey:id */ + avatarId?: string | null; + avatarDecorations?: ({ + /** Format: misskey:id */ + id: string; + angle?: number | null; + flipH?: boolean | null; + })[]; + /** Format: misskey:id */ + bannerId?: string | null; + fields?: { + name: string; + value: string; + }[]; + isLocked?: boolean; + isExplorable?: boolean; + hideOnlineStatus?: boolean; + publicReactions?: boolean; + carefulBot?: boolean; + autoAcceptFollowed?: boolean; + noCrawle?: boolean; + preventAiLearning?: boolean; + isBot?: boolean; + isCat?: boolean; + injectFeaturedNote?: boolean; + receiveAnnouncementEmail?: boolean; + alwaysMarkNsfw?: boolean; + autoSensitive?: boolean; + /** @enum {string} */ + ffVisibility?: 'public' | 'followers' | 'private'; + /** Format: misskey:id */ + pinnedPageId?: string | null; + mutedWords?: (string[] | string)[]; + hardMutedWords?: (string[] | string)[]; + mutedInstances?: string[]; + notificationRecieveConfig?: Record<string, never>; + emailNotificationTypes?: string[]; + alsoKnownAs?: string[]; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['MeDetailed']; + }; + }; + /** @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/move + * @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/move': { + requestBody: { + content: { + 'application/json': { + moveToAccount: string; + }; + }; + }; + 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/webhooks/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'i/webhooks/create': { + requestBody: { + content: { + 'application/json': { + name: string; + url: string; + /** @default */ + secret?: string; + on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/webhooks/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + 'i/webhooks/list': { + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/webhooks/show + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + 'i/webhooks/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + webhookId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/webhooks/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'i/webhooks/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + webhookId: string; + name: string; + url: string; + /** @default */ + secret?: string; + on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; + active: boolean; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * i/webhooks/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'i/webhooks/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + webhookId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * invite/create + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'invite/create': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['InviteCode']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * invite/delete + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'invite/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + inviteId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * invite/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'invite/list': { + requestBody: { + content: { + 'application/json': { + /** @default 30 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['InviteCode'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * invite/limit + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'invite/limit': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + remaining: number | null; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * meta + * @description No description provided. + * + * **Credential required**: *No* + */ + meta: { + requestBody: { + content: { + 'application/json': { + /** @default true */ + detail?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + maintainerName: string | null; + maintainerEmail: string | null; + version: string; + name: string; + shortName: string | null; + /** + * Format: url + * @example https://misskey.example.com + */ + uri: string; + description: string | null; + langs: string[]; + tosUrl: string | null; + /** @default https://github.com/misskey-dev/misskey */ + repositoryUrl: string; + /** @default https://github.com/misskey-dev/misskey/issues/new */ + feedbackUrl: string; + defaultDarkTheme: string | null; + defaultLightTheme: string | null; + disableRegistration: boolean; + cacheRemoteFiles: boolean; + cacheRemoteSensitiveFiles: boolean; + emailRequiredForSignup: boolean; + enableHcaptcha: boolean; + hcaptchaSiteKey: string | null; + enableRecaptcha: boolean; + recaptchaSiteKey: string | null; + enableTurnstile: boolean; + turnstileSiteKey: string | null; + swPublickey: string | null; + /** @default /assets/ai.png */ + mascotImageUrl: string; + bannerUrl: string; + serverErrorImageUrl: string | null; + infoImageUrl: string | null; + notFoundImageUrl: string | null; + iconUrl: string | null; + maxNoteTextLength: number; + ads: { + place: string; + /** Format: url */ + url: string; + /** Format: url */ + imageUrl: string; + }[]; + /** @default 0 */ + notesPerOneAd: number; + /** @example false */ + requireSetup: boolean; + enableEmail: boolean; + enableServiceWorker: boolean; + translatorAvailable: boolean; + proxyAccountName: string | null; + mediaProxy: string; + features?: { + registration: boolean; + localTimeline: boolean; + globalTimeline: boolean; + hcaptcha: boolean; + recaptcha: boolean; + objectStorage: boolean; + serviceWorker: boolean; + /** @default true */ + miauth?: boolean; + }; + backgroundImageUrl: string | null; + impressumUrl: string | null; + logoImageUrl: string | null; + privacyPolicyUrl: string | null; + serverRules: string[]; + themeColor: string | null; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * emojis + * @description No description provided. + * + * **Credential required**: *No* + */ + emojis: { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + emojis: components['schemas']['EmojiSimple'][]; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * emoji + * @description No description provided. + * + * **Credential required**: *No* + */ + emoji: { + requestBody: { + content: { + 'application/json': { + name: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['EmojiDetailed']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * miauth/gen-token + * @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* + */ + 'miauth/gen-token': { + requestBody: { + content: { + 'application/json': { + session: string | null; + name?: string | null; + description?: string | null; + iconUrl?: string | null; + permission: string[]; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + token: string; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * mute/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:mutes* + */ + 'mute/create': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + /** @description A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute. */ + expiresAt?: number | null; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * mute/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:mutes* + */ + 'mute/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * mute/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:mutes* + */ + 'mute/list': { + requestBody: { + content: { + 'application/json': { + /** @default 30 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Muting'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * renote-mute/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:mutes* + */ + 'renote-mute/create': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * renote-mute/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:mutes* + */ + 'renote-mute/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * renote-mute/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:mutes* + */ + 'renote-mute/list': { + requestBody: { + content: { + 'application/json': { + /** @default 30 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['RenoteMuting'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * my/apps + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'my/apps': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** @default 0 */ + offset?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['App'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes + * @description No description provided. + * + * **Credential required**: *No* + */ + notes: { + requestBody: { + content: { + 'application/json': { + /** @default false */ + local?: boolean; + reply?: boolean; + renote?: boolean; + withFiles?: boolean; + poll?: boolean; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/children + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/children': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/clips + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/clips': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Clip'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/conversation + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/conversation': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + /** @default 10 */ + limit?: number; + /** @default 0 */ + offset?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + 'notes/create': { + requestBody: { + content: { + 'application/json': { + /** + * @default public + * @enum {string} + */ + visibility?: 'public' | 'home' | 'followers' | 'specified'; + visibleUserIds?: string[]; + cw?: string | null; + /** @default false */ + localOnly?: boolean; + /** + * @default null + * @enum {string|null} + */ + reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote'; + /** @default false */ + noExtractMentions?: boolean; + /** @default false */ + noExtractHashtags?: boolean; + /** @default false */ + noExtractEmojis?: boolean; + /** Format: misskey:id */ + replyId?: string | null; + /** Format: misskey:id */ + renoteId?: string | null; + /** Format: misskey:id */ + channelId?: string | null; + text?: string | null; + fileIds?: string[]; + mediaIds?: string[]; + poll?: ({ + choices: string[]; + multiple?: boolean; + expiresAt?: number | null; + expiredAfter?: number | null; + }) | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + createdNote: components['schemas']['Note']; + }; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * notes/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + 'notes/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * notes/favorites/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:favorites* + */ + 'notes/favorites/create': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * notes/favorites/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:favorites* + */ + 'notes/favorites/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/featured + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/featured': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + untilId?: string; + /** Format: misskey:id */ + channelId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/global-timeline + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/global-timeline': { + requestBody: { + content: { + 'application/json': { + /** @default false */ + withFiles?: boolean; + /** @default true */ + withRenotes?: boolean; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/hybrid-timeline + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'notes/hybrid-timeline': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + /** @default false */ + allowPartial?: boolean; + /** @default true */ + includeMyRenotes?: boolean; + /** @default true */ + includeRenotedMyNotes?: boolean; + /** @default true */ + includeLocalRenotes?: boolean; + /** @default false */ + withFiles?: boolean; + /** @default true */ + withRenotes?: boolean; + /** @default false */ + withReplies?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/local-timeline + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/local-timeline': { + requestBody: { + content: { + 'application/json': { + /** @default false */ + withFiles?: boolean; + /** @default true */ + withRenotes?: boolean; + /** @default false */ + withReplies?: boolean; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default false */ + allowPartial?: boolean; + sinceDate?: number; + untilDate?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/mentions + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'notes/mentions': { + requestBody: { + content: { + 'application/json': { + /** @default false */ + following?: boolean; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + visibility?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/polls/recommendation + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'notes/polls/recommendation': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** @default 0 */ + offset?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/polls/vote + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:votes* + */ + 'notes/polls/vote': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + choice: number; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/reactions + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/reactions': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + type?: string | null; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['NoteReaction'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/reactions/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:reactions* + */ + 'notes/reactions/create': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + reaction: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/reactions/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:reactions* + */ + 'notes/reactions/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * notes/renotes + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/renotes': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/replies + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/replies': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/search-by-tag + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/search-by-tag': { + requestBody: { + content: { + 'application/json': { + /** @default null */ + reply?: boolean | null; + /** @default null */ + renote?: boolean | null; + /** + * @description Only show notes that have attached files. + * @default false + */ + withFiles?: boolean; + /** @default null */ + poll?: boolean | null; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + tag?: string; + /** @description The outer arrays are chained with OR, the inner arrays are chained with AND. */ + query?: string[][]; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/search + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/search': { + requestBody: { + content: { + 'application/json': { + query: string; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + /** @default 0 */ + offset?: number; + /** @description The local host is represented with `.`. */ + host?: string; + /** + * Format: misskey:id + * @default null + */ + userId?: string | null; + /** + * Format: misskey:id + * @default null + */ + channelId?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/show + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/state + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'notes/state': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + isFavorited: boolean; + isMutedThread: boolean; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/thread-muting/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'notes/thread-muting/create': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * notes/thread-muting/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'notes/thread-muting/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/timeline + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'notes/timeline': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + /** @default false */ + allowPartial?: boolean; + /** @default true */ + includeMyRenotes?: boolean; + /** @default true */ + includeRenotedMyNotes?: boolean; + /** @default true */ + includeLocalRenotes?: boolean; + /** @default false */ + withFiles?: boolean; + /** @default true */ + withRenotes?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/translate + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'notes/translate': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + targetLang: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': Record<string, 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/unrenote + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + 'notes/unrenote': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * notes/user-list-timeline + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'notes/user-list-timeline': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + listId: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + /** @default false */ + allowPartial?: boolean; + /** @default true */ + includeMyRenotes?: boolean; + /** @default true */ + includeRenotedMyNotes?: boolean; + /** @default true */ + includeLocalRenotes?: boolean; + /** @default true */ + withRenotes?: boolean; + /** + * @description Only show notes that have attached files. + * @default false + */ + withFiles?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notifications/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notifications* + */ + 'notifications/create': { + requestBody: { + content: { + 'application/json': { + body: string; + header?: string | null; + icon?: string | null; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * notifications/mark-all-as-read + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notifications* + */ + 'notifications/mark-all-as-read': { + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notifications/test-notification + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notifications* + */ + 'notifications/test-notification': { + 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']; + }; + }; + }; + }; + /** + * page-push + * @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* + */ + 'page-push': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + pageId: string; + event: string; + var?: unknown; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * pages/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:pages* + */ + 'pages/create': { + requestBody: { + content: { + 'application/json': { + title: string; + name: string; + summary?: string | null; + content: { + [key: string]: unknown; + }[]; + variables: { + [key: string]: unknown; + }[]; + script: string; + /** Format: misskey:id */ + eyeCatchingImageId?: string | null; + /** + * @default sans-serif + * @enum {string} + */ + font?: 'serif' | 'sans-serif'; + /** @default false */ + alignCenter?: boolean; + /** @default false */ + hideTitleWhenPinned?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Page']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * pages/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:pages* + */ + 'pages/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + pageId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * pages/featured + * @description No description provided. + * + * **Credential required**: *No* + */ + 'pages/featured': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Page'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * pages/like + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:page-likes* + */ + 'pages/like': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + pageId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * pages/show + * @description No description provided. + * + * **Credential required**: *No* + */ + 'pages/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + pageId?: string; + name?: string; + username?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Page']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * pages/unlike + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:page-likes* + */ + 'pages/unlike': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + pageId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * pages/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:pages* + */ + 'pages/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + pageId: string; + title: string; + name: string; + summary?: string | null; + content: { + [key: string]: unknown; + }[]; + variables: { + [key: string]: unknown; + }[]; + script: string; + /** Format: misskey:id */ + eyeCatchingImageId?: string | null; + /** @enum {string} */ + font?: 'serif' | 'sans-serif'; + alignCenter?: boolean; + hideTitleWhenPinned?: boolean; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * flash/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash* + */ + 'flash/create': { + requestBody: { + content: { + 'application/json': { + title: string; + summary: string; + script: string; + permissions: string[]; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * flash/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash* + */ + 'flash/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + flashId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * flash/featured + * @description No description provided. + * + * **Credential required**: *No* + */ + 'flash/featured': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Flash'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * flash/like + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash-likes* + */ + 'flash/like': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + flashId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * flash/show + * @description No description provided. + * + * **Credential required**: *No* + */ + 'flash/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + flashId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Flash']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * flash/unlike + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash-likes* + */ + 'flash/unlike': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + flashId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * flash/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:flash* + */ + 'flash/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + flashId: string; + title: string; + summary: string; + script: string; + permissions: string[]; + /** @enum {string} */ + visibility?: 'public' | 'private'; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * flash/my + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:flash* + */ + 'flash/my': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Flash'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * flash/my-likes + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:flash-likes* + */ + 'flash/my-likes': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** Format: id */ + id: string; + flash: components['schemas']['Flash']; + }[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * ping + * @description No description provided. + * + * **Credential required**: *No* + */ + ping: { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + pong: number; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * pinned-users + * @description No description provided. + * + * **Credential required**: *No* + */ + 'pinned-users': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserDetailed'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * promo/read + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'promo/read': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * roles/list + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'roles/list': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Role'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * roles/show + * @description No description provided. + * + * **Credential required**: *No* + */ + 'roles/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roleId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Role']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * roles/users + * @description No description provided. + * + * **Credential required**: *No* + */ + 'roles/users': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roleId: string; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * roles/notes + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'roles/notes': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roleId: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * request-reset-password + * @description Request a users password to be reset. + * + * **Credential required**: *No* + */ + 'request-reset-password': { + requestBody: { + content: { + 'application/json': { + username: string; + email: string; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * reset-db + * @description Only available when running with <code>NODE_ENV=testing</code>. Reset the database and flush Redis. + * + * **Credential required**: *No* + */ + 'reset-db': { + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * reset-password + * @description Complete the password reset that was previously requested. + * + * **Credential required**: *No* + */ + 'reset-password': { + requestBody: { + content: { + 'application/json': { + token: string; + password: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * server-info + * @description No description provided. + * + * **Credential required**: *No* + */ + 'server-info': { + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * stats + * @description No description provided. + * + * **Credential required**: *No* + */ + stats: { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + notesCount: number; + originalNotesCount: number; + usersCount: number; + originalUsersCount: number; + instances: number; + driveUsageLocal: number; + driveUsageRemote: number; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * sw/show-registration + * @description Check push notification registration exists. + * + * **Credential required**: *Yes* + */ + 'sw/show-registration': { + requestBody: { + content: { + 'application/json': { + endpoint: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + userId: string; + endpoint: string; + sendReadMessage: boolean; + } | null; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * sw/update-registration + * @description Update push notification registration. + * + * **Credential required**: *Yes* + */ + 'sw/update-registration': { + requestBody: { + content: { + 'application/json': { + endpoint: string; + sendReadMessage?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + userId: string; + endpoint: string; + sendReadMessage: boolean; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * sw/register + * @description Register to receive push notifications. + * + * **Credential required**: *Yes* + */ + 'sw/register': { + requestBody: { + content: { + 'application/json': { + endpoint: string; + auth: string; + publickey: string; + /** @default false */ + sendReadMessage?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** @enum {string} */ + state?: 'already-subscribed' | 'subscribed'; + key: string | null; + userId: string; + endpoint: string; + sendReadMessage: boolean; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * sw/unregister + * @description Unregister from receiving push notifications. + * + * **Credential required**: *No* + */ + 'sw/unregister': { + requestBody: { + content: { + 'application/json': { + endpoint: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * test + * @description Endpoint for testing input validation. + * + * **Credential required**: *No* + */ + test: { + requestBody: { + content: { + 'application/json': { + required: boolean; + string?: string; + /** @default hello */ + default?: string; + /** @default hello */ + nullableDefault?: string | null; + /** Format: misskey:id */ + id?: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * username/available + * @description No description provided. + * + * **Credential required**: *No* + */ + 'username/available': { + requestBody: { + content: { + 'application/json': { + username: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + available: boolean; + }; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users + * @description No description provided. + * + * **Credential required**: *No* + */ + users: { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** @default 0 */ + offset?: number; + /** @enum {string} */ + sort?: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt'; + /** + * @default all + * @enum {string} + */ + state?: 'all' | 'alive'; + /** + * @default local + * @enum {string} + */ + origin?: 'combined' | 'local' | 'remote'; + /** + * @description The local host is represented with `null`. + * @default null + */ + hostname?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserDetailed'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/clips + * @description Show all clips this user owns. + * + * **Credential required**: *No* + */ + 'users/clips': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Clip'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/followers + * @description Show everyone that follows this user. + * + * **Credential required**: *No* + */ + 'users/followers': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + userId?: string; + username?: string; + /** @description The local host is represented with `null`. */ + host?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Following'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/following + * @description Show everyone that this user is following. + * + * **Credential required**: *No* + */ + 'users/following': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + userId?: string; + username?: string; + /** @description The local host is represented with `null`. */ + host?: string | null; + birthday?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Following'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/gallery/posts + * @description Show all gallery posts by the given user. + * + * **Credential required**: *No* + */ + 'users/gallery/posts': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['GalleryPost'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/get-frequently-replied-users + * @description Get a list of other users that the specified user frequently replies to. + * + * **Credential required**: *No* + */ + 'users/get-frequently-replied-users': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + /** @default 10 */ + limit?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + user: components['schemas']['UserDetailed']; + weight: number; + }[]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/featured-notes + * @description No description provided. + * + * **Credential required**: *No* + */ + 'users/featured-notes': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + untilId?: string; + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/lists/create + * @description Create a new list of users. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'users/lists/create': { + requestBody: { + content: { + 'application/json': { + name: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserList']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/lists/delete + * @description Delete an existing list of users. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'users/lists/delete': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + listId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/lists/list + * @description Show all lists that the authenticated user has created. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + 'users/lists/list': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserList'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/lists/pull + * @description Remove a user from a list. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'users/lists/pull': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + listId: string; + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/lists/push + * @description Add a user to an existing list. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'users/lists/push': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + listId: string; + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * users/lists/show + * @description Show the properties of a list. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + 'users/lists/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + listId: string; + /** @default false */ + forPublic?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserList']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/lists/favorite + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'users/lists/favorite': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + listId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/lists/unfavorite + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'users/lists/unfavorite': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + listId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/lists/update + * @description Update the properties of a list. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'users/lists/update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + listId: string; + name?: string; + isPublic?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserList']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/lists/create-from-public + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'users/lists/create-from-public': { + requestBody: { + content: { + 'application/json': { + name: string; + /** Format: misskey:id */ + listId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserList']; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/lists/update-membership + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'users/lists/update-membership': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + listId: string; + /** Format: misskey:id */ + userId: string; + withReplies?: boolean; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/lists/get-memberships + * @description No description provided. + * + * **Credential required**: *No* / **Permission**: *read:account* + */ + 'users/lists/get-memberships': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + listId: string; + /** @default false */ + forPublic?: boolean; + /** @default 30 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/notes + * @description No description provided. + * + * **Credential required**: *No* + */ + 'users/notes': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + /** @default false */ + withReplies?: boolean; + /** @default true */ + withRenotes?: boolean; + /** @default false */ + withChannelNotes?: boolean; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + /** @default false */ + allowPartial?: boolean; + /** @default false */ + withFiles?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/pages + * @description Show all pages this user created. + * + * **Credential required**: *No* + */ + 'users/pages': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Page'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/flashs + * @description Show all flashs this user created. + * + * **Credential required**: *No* + */ + 'users/flashs': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Flash'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/reactions + * @description Show all reactions this user made. + * + * **Credential required**: *No* + */ + 'users/reactions': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['NoteReaction'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/recommendation + * @description Show users that the authenticated user might be interested to follow. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + 'users/recommendation': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** @default 0 */ + offset?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserDetailed'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/relation + * @description Show the different kinds of relations between the authenticated user and the specified user(s). + * + * **Credential required**: *Yes* + */ + 'users/relation': { + requestBody: { + content: { + 'application/json': { + userId: string | string[]; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': OneOf<[{ + /** Format: id */ + id: string; + isFollowing: boolean; + hasPendingFollowRequestFromYou: boolean; + hasPendingFollowRequestToYou: boolean; + isFollowed: boolean; + isBlocking: boolean; + isBlocked: boolean; + isMuted: boolean; + isRenoteMuted: boolean; + }, { + /** Format: id */ + id: string; + isFollowing: boolean; + hasPendingFollowRequestFromYou: boolean; + hasPendingFollowRequestToYou: boolean; + isFollowed: boolean; + isBlocking: boolean; + isBlocked: boolean; + isMuted: boolean; + isRenoteMuted: boolean; + }[]]>; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/report-abuse + * @description File a report. + * + * **Credential required**: *Yes* + */ + 'users/report-abuse': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + comment: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/search-by-username-and-host + * @description Search for a user by username and/or host. + * + * **Credential required**: *No* + */ + 'users/search-by-username-and-host': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** @default true */ + detail?: boolean; + username?: string | null; + host?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['User'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/search + * @description Search for users. + * + * **Credential required**: *No* + */ + 'users/search': { + requestBody: { + content: { + 'application/json': { + query: string; + /** @default 0 */ + offset?: number; + /** @default 10 */ + limit?: number; + /** + * @default combined + * @enum {string} + */ + origin?: 'local' | 'remote' | 'combined'; + /** @default true */ + detail?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['User'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/show + * @description Show the properties of a user. + * + * **Credential required**: *No* + */ + 'users/show': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId?: string; + userIds?: string[]; + username?: string; + /** @description The local host is represented with `null`. */ + host?: string | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserDetailed'] | components['schemas']['UserDetailed'][]; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/achievements + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'users/achievements': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * users/update-memo + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'users/update-memo': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + /** @description A personal memo for the target user. If null or empty, delete the memo. */ + memo: string | null; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * fetch-rss + * @description No description provided. + * + * **Credential required**: *No* + */ + 'fetch-rss': { + requestBody: { + content: { + 'application/json': { + url: string; + }; + }; + }; + 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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * fetch-external-resources + * @description No description provided. + * + * **Credential required**: *Yes* + */ + 'fetch-external-resources': { + requestBody: { + content: { + 'application/json': { + url: string; + hash: string; + }; + }; + }; + 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']; + }; + }; + }; + }; + /** + * retention + * @description No description provided. + * + * **Credential required**: *No* + */ + retention: { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': unknown; + }; + }; + /** @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 Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; +}; + diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 027d7ac944..069f5f9499 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -1,10 +1,12 @@ -export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'achievementEarned'] as const; +export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; -export const ffVisibility = ['public', 'followers', 'private'] as const; +export const followingVisibilities = ['public', 'followers', 'private'] as const; + +export const followersVisibilities = ['public', 'followers', 'private'] as const; export const permissions = [ 'read:account', @@ -82,6 +84,8 @@ export const moderationLogTypes = [ 'createAvatarDecoration', 'updateAvatarDecoration', 'deleteAvatarDecoration', + 'unsetUserAvatar', + 'unsetUserBanner', ] as const; export type ModerationLogPayloads = { @@ -261,4 +265,16 @@ export type ModerationLogPayloads = { avatarDecorationId: string; avatarDecoration: any; }; + unsetUserAvatar: { + userId: string; + userUsername: string; + userHost: string | null; + fileId: string; + }; + unsetUserBanner: { + userId: string; + userUsername: string; + userHost: string | null; + fileId: string; + }; }; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 1a4509cabf..2c490fa55e 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -1,480 +1,12 @@ -import { ModerationLogPayloads, notificationTypes } from './consts.js'; +import { ModerationLogPayloads } from './consts.js'; +import { Announcement, EmojiDetailed, Page, User, UserDetailed } from './autogen/models'; + +export * from './autogen/entities'; +export * from './autogen/models'; export type ID = string; export type DateString = string; -type TODO = Record<string, any>; - -// NOTE: 極力この型を使うのは避け、UserLite か UserDetailed か明示するように -export type User = UserLite | UserDetailed; - -export type UserLite = { - id: ID; - username: string; - host: string | null; - name: string | null; - onlineStatus: 'online' | 'active' | 'offline' | 'unknown'; - avatarUrl: string; - avatarBlurhash: string; - approved: boolean; - avatarDecorations: { - id: ID; - url: string; - angle?: number; - flipH?: boolean; - }[]; - emojis: { - name: string; - url: string; - }[]; - instance?: { - name: Instance['name']; - softwareName: Instance['softwareName']; - softwareVersion: Instance['softwareVersion']; - iconUrl: Instance['iconUrl']; - faviconUrl: Instance['faviconUrl']; - themeColor: Instance['themeColor']; - }; - isCat?: boolean; - isBot?: boolean; - noindex?: boolean; -}; - -export type UserDetailed = UserLite & { - alsoKnownAs: string[]; - bannerBlurhash: string | null; - bannerColor: string | null; - bannerUrl: string | null; - backgroundUrl: string | null; - backgroundBlurhash: string | null; - birthday: string | null; - listenbrainz: string | null; - createdAt: DateString; - description: string | null; - ffVisibility: 'public' | 'followers' | 'private'; - fields: {name: string; value: string}[]; - verifiedLinks: string[]; - followersCount: number; - followingCount: number; - hasPendingFollowRequestFromYou: boolean; - hasPendingFollowRequestToYou: boolean; - isAdmin: boolean; - isBlocked: boolean; - isBlocking: boolean; - isBot: boolean; - isCat: boolean; - speakAsCat: boolean; - isFollowed: boolean; - isFollowing: boolean; - noindex: boolean; - isLocked: boolean; - isModerator: boolean; - isMuted: boolean; - isSilenced: boolean; - isSuspended: boolean; - lang: string | null; - lastFetchedAt?: DateString; - location: string | null; - movedTo: string; - notesCount: number; - pinnedNoteIds: ID[]; - pinnedNotes: Note[]; - pinnedPage: Page | null; - pinnedPageId: string | null; - publicReactions: boolean; - securityKeys: boolean; - twoFactorEnabled: boolean; - updatedAt: DateString | null; - uri: string | null; - url: string | null; - notify: 'normal' | 'none'; -}; - -export type UserGroup = TODO; - -export type UserList = { - id: ID; - createdAt: DateString; - name: string; - userIds: User['id'][]; -}; - -export type MeDetailed = UserDetailed & { - avatarId: DriveFile['id']; - bannerId: DriveFile['id']; - backgroundId: DriveFile['id']; - autoAcceptFollowed: boolean; - alwaysMarkNsfw: boolean; - carefulBot: boolean; - emailNotificationTypes: string[]; - hasPendingReceivedFollowRequest: boolean; - hasUnreadAnnouncement: boolean; - hasUnreadAntenna: boolean; - hasUnreadMentions: boolean; - hasUnreadMessagingMessage: boolean; - hasUnreadNotification: boolean; - hasUnreadSpecifiedNotes: boolean; - unreadNotificationsCount: number; - hideOnlineStatus: boolean; - injectFeaturedNote: boolean; - integrations: Record<string, any>; - isDeleted: boolean; - isExplorable: boolean; - mutedWords: string[][]; - notificationRecieveConfig: { - [notificationType in typeof notificationTypes[number]]?: { - type: 'all'; - } | { - type: 'never'; - } | { - type: 'following'; - } | { - type: 'follower'; - } | { - type: 'mutualFollow'; - } | { - type: 'list'; - userListId: string; - }; - }; - noCrawle: boolean; - receiveAnnouncementEmail: boolean; - usePasswordLessLogin: boolean; - unreadAnnouncements: Announcement[]; - twoFactorBackupCodesStock: 'full' | 'partial' | 'none'; - [other: string]: any; -}; - -export type MeDetailedWithSecret = MeDetailed & { - email: string; - emailVerified: boolean; - securityKeysList: { - id: string; - name: string; - lastUsed: string; - }[]; -}; - -export type MeSignup = MeDetailedWithSecret & { - token: string; -}; - -export type DriveFile = { - id: ID; - createdAt: DateString; - isSensitive: boolean; - name: string; - thumbnailUrl: string; - url: string; - type: string; - size: number; - md5: string; - blurhash: string; - comment: string | null; - properties: Record<string, any>; -}; - -export type DriveFolder = TODO; - -export type GalleryPost = { - id: ID; - createdAt: DateString; - updatedAt: DateString; - userId: User['id']; - user: User; - title: string; - description: string | null; - fileIds: DriveFile['id'][]; - files: DriveFile[]; - isSensitive: boolean; - likedCount: number; - isLiked?: boolean; -}; - -export type Note = { - id: ID; - createdAt: DateString; - updatedAt?: DateString | null; - text: string | null; - cw: string | null; - user: User; - userId: User['id']; - reply?: Note; - replyId: Note['id']; - renote?: Note; - renoteId: Note['id']; - files: DriveFile[]; - fileIds: DriveFile['id'][]; - visibility: 'public' | 'home' | 'followers' | 'specified'; - visibleUserIds?: User['id'][]; - channel?: Channel; - channelId?: Channel['id']; - localOnly?: boolean; - myReaction?: string; - reactions: Record<string, number>; - renoteCount: number; - repliesCount: number; - clippedCount?: number; - poll?: { - expiresAt: DateString | null; - expiredAfter: DateString | null; - multiple: boolean; - choices: { - isVoted: boolean; - text: string; - votes: number; - }[]; - }; - emojis: { - name: string; - url: string; - }[]; - uri?: string; - url?: string; - isHidden?: boolean; -}; - -export type NoteReaction = { - id: ID; - createdAt: DateString; - user: UserLite; - type: string; -}; - -export type NoteEdit = { - noteId: Note['id']; - note: Note; - newText: string; - oldText: string; - cw: string; - fileIds: DriveFile['id'][]; - updatedAt?: DateString; - oldDate: DateString; -} - -export type Notification = { - id: ID; - createdAt: DateString; - isRead: boolean; -} & ({ - type: 'reaction'; - reaction: string; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'reply'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'renote'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'quote'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'mention'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'note'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'pollEnded'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'follow'; - user: User; - userId: User['id']; -} | { - type: 'followRequestAccepted'; - user: User; - userId: User['id']; -} | { - type: 'receiveFollowRequest'; - user: User; - userId: User['id']; -} | { - type: 'groupInvited'; - invitation: UserGroup; - user: User; - userId: User['id']; -} | { - type: 'achievementEarned'; - achievement: string; -} | { - type: 'app'; - header?: string | null; - body: string; - icon?: string | null; -} | { - type: 'test'; -}); - -export type MessagingMessage = { - id: ID; - createdAt: DateString; - file: DriveFile | null; - fileId: DriveFile['id'] | null; - isRead: boolean; - reads: User['id'][]; - text: string | null; - user: User; - userId: User['id']; - recipient?: User | null; - recipientId: User['id'] | null; - group?: UserGroup | null; - groupId: UserGroup['id'] | null; -}; - -export type CustomEmoji = { - id: string; - name: string; - url: string; - category: string; - aliases: string[]; -}; - -export type LiteInstanceMetadata = { - maintainerName: string | null; - maintainerEmail: string | null; - version: string; - name: string | null; - shortName: string | null; - uri: string; - description: string | null; - langs: string[]; - tosUrl: string | null; - repositoryUrl: string; - feedbackUrl: string; - impressumUrl: string | null; - privacyPolicyUrl: string | null; - disableRegistration: boolean; - disableLocalTimeline: boolean; - disableBubbleTimeline: boolean; - disableGlobalTimeline: boolean; - driveCapacityPerLocalUserMb: number; - driveCapacityPerRemoteUserMb: number; - emailRequiredForSignup: boolean; - approvalRequiredForSignup: boolean; - enableHcaptcha: boolean; - hcaptchaSiteKey: string | null; - enableRecaptcha: boolean; - recaptchaSiteKey: string | null; - enableTurnstile: boolean; - turnstileSiteKey: string | null; - swPublickey: string | null; - themeColor: string | null; - mascotImageUrl: string | null; - bannerUrl: string | null; - serverErrorImageUrl: string | null; - infoImageUrl: string | null; - notFoundImageUrl: string | null; - iconUrl: string | null; - backgroundImageUrl: string | null; - logoImageUrl: string | null; - maxNoteTextLength: number; - enableEmail: boolean; - enableTwitterIntegration: boolean; - enableGithubIntegration: boolean; - enableDiscordIntegration: boolean; - enableAchievements: boolean; - enableServiceWorker: boolean; - emojis: CustomEmoji[]; - defaultDarkTheme: string | null; - defaultLightTheme: string | null; - ads: { - id: ID; - ratio: number; - place: string; - url: string; - imageUrl: string; - }[]; - notesPerOneAd: number; - translatorAvailable: boolean; - serverRules: string[]; - defaultLike: string; -}; - -export type DetailedInstanceMetadata = LiteInstanceMetadata & { - pinnedPages: string[]; - pinnedClipId: string | null; - cacheRemoteFiles: boolean; - cacheRemoteSensitiveFiles: boolean; - requireSetup: boolean; - proxyAccountName: string | null; - features: Record<string, any>; -}; - -export type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata; - -export type AdminInstanceMetadata = DetailedInstanceMetadata & { - // TODO: There are more fields. - blockedHosts: string[]; - silencedHosts: string[]; - app192IconUrl: string | null; - app512IconUrl: string | null; - manifestJsonOverride: string; - enableBotTrending: boolean; -}; - -export type ServerInfo = { - machine: string; - cpu: { - model: string; - cores: number; - }; - mem: { - total: number; - }; - fs: { - total: number; - used: number; - }; -}; - -export type Stats = { - notesCount: number; - originalNotesCount: number; - usersCount: number; - originalUsersCount: number; - instances: number; - driveUsageLocal: number; - driveUsageRemote: number; -}; - -export type Page = { - id: ID; - createdAt: DateString; - updatedAt: DateString; - userId: User['id']; - user: User; - content: Record<string, any>[]; - variables: Record<string, any>[]; - title: string; - name: string; - summary: string | null; - hideTitleWhenPinned: boolean; - alignCenter: boolean; - font: string; - script: string; - eyeCatchingImageId: DriveFile['id'] | null; - eyeCatchingImage: DriveFile | null; - attachedFiles: any; - likedCount: number; - isLiked?: boolean; -}; - export type PageEvent = { pageId: Page['id']; event: string; @@ -483,167 +15,6 @@ export type PageEvent = { user: User; }; -export type Announcement = { - id: ID; - createdAt: DateString; - updatedAt: DateString | null; - text: string; - title: string; - imageUrl: string | null; - display: 'normal' | 'banner' | 'dialog'; - icon: 'info' | 'warning' | 'error' | 'success'; - needConfirmationToRead: boolean; - forYou: boolean; - isRead?: boolean; -}; - -export type Antenna = { - id: ID; - createdAt: DateString; - name: string; - keywords: string[][]; // TODO - excludeKeywords: string[][]; // TODO - src: 'home' | 'all' | 'users' | 'list' | 'group'; - userListId: ID | null; // TODO - userGroupId: ID | null; // TODO - users: string[]; // TODO - caseSensitive: boolean; - localOnly: boolean; - notify: boolean; - withReplies: boolean; - withFile: boolean; - hasUnreadNote: boolean; -}; - -export type App = TODO; - -export type AuthSession = { - id: ID; - app: App; - token: string; -}; - -export type Ad = TODO; - -export type Clip = TODO; - -export type NoteFavorite = { - id: ID; - createdAt: DateString; - noteId: Note['id']; - note: Note; -}; - -export type FollowRequest = { - id: ID; - follower: User; - followee: User; -}; - -export type Channel = { - id: ID; - lastNotedAt: Date | null; - userId: User['id'] | null; - user: User | null; - name: string; - description: string | null; - bannerId: DriveFile['id'] | null; - banner: DriveFile | null; - pinnedNoteIds: string[]; - color: string; - isArchived: boolean; - notesCount: number; - usersCount: number; - isSensitive: boolean; - allowRenoteToExternal: boolean; -}; - -export type Following = { - id: ID; - createdAt: DateString; - followerId: User['id']; - followeeId: User['id']; -}; - -export type FollowingFolloweePopulated = Following & { - followee: UserDetailed; -}; - -export type FollowingFollowerPopulated = Following & { - follower: UserDetailed; -}; - -export type Blocking = { - id: ID; - createdAt: DateString; - blockeeId: User['id']; - blockee: UserDetailed; -}; - -export type Instance = { - id: ID; - firstRetrievedAt: DateString; - host: string; - usersCount: number; - notesCount: number; - followingCount: number; - followersCount: number; - driveUsage: number; - driveFiles: number; - latestRequestSentAt: DateString | null; - latestStatus: number | null; - latestRequestReceivedAt: DateString | null; - lastCommunicatedAt: DateString; - isNotResponding: boolean; - isSuspended: boolean; - isSilenced: boolean; - isBlocked: boolean; - softwareName: string | null; - softwareVersion: string | null; - openRegistrations: boolean | null; - name: string | null; - description: string | null; - maintainerName: string | null; - maintainerEmail: string | null; - iconUrl: string | null; - faviconUrl: string | null; - themeColor: string | null; - infoUpdatedAt: DateString | null; - isNSFW: boolean; -}; - -export type Signin = { - id: ID; - createdAt: DateString; - ip: string; - headers: Record<string, any>; - success: boolean; -}; - -export type Invite = { - id: ID; - code: string; - expiresAt: DateString | null; - createdAt: DateString; - createdBy: UserLite | null; - usedBy: UserLite | null; - usedAt: DateString | null; - used: boolean; -} - -export type InviteLimit = { - remaining: number; -} - -export type UserSorting = - | '+follower' - | '-follower' - | '+createdAt' - | '-createdAt' - | '+updatedAt' - | '-updatedAt'; -export type OriginType = 'combined' | 'local' | 'remote'; - export type ModerationLog = { id: ID; createdAt: DateString; @@ -757,4 +128,61 @@ export type ModerationLog = { } | { type: 'resolveAbuseReport'; info: ModerationLogPayloads['resolveAbuseReport']; +} | { + type: 'unsetUserAvatar'; + info: ModerationLogPayloads['unsetUserAvatar']; +} | { + type: 'unsetUserBanner'; + info: ModerationLogPayloads['unsetUserBanner']; }); + +export type ServerStats = { + cpu: number; + mem: { + used: number; + active: number; + }; + net: { + rx: number; + tx: number; + }; + fs: { + r: number; + w: number; + } +}; + +export type ServerStatsLog = string[]; + +export type QueueStats = { + deliver: { + activeSincePrevTick: number; + active: number; + waiting: number; + delayed: number; + }; + inbox: { + activeSincePrevTick: number; + active: number; + waiting: number; + delayed: number; + }; +}; + +export type QueueStatsLog = string[]; + +export type EmojiAdded = { + emoji: EmojiDetailed +}; + +export type EmojiUpdated = { + emojis: EmojiDetailed[] +}; + +export type EmojiDeleted = { + emojis: EmojiDetailed[] +}; + +export type AnnouncementCreated = { + announcement: Announcement; +}; diff --git a/packages/misskey-js/src/index.ts b/packages/misskey-js/src/index.ts index e78501fdfd..54cae8ec03 100644 --- a/packages/misskey-js/src/index.ts +++ b/packages/misskey-js/src/index.ts @@ -16,7 +16,8 @@ export const permissions = consts.permissions; export const notificationTypes = consts.notificationTypes; export const noteVisibilities = consts.noteVisibilities; export const mutedNoteReasons = consts.mutedNoteReasons; -export const ffVisibility = consts.ffVisibility; +export const followingVisibilities = consts.followingVisibilities; +export const followersVisibilities = consts.followersVisibilities; export const moderationLogTypes = consts.moderationLogTypes; // api extractor not supported yet diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index a6688e3866..e2b550ab7a 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -1,6 +1,23 @@ -import type { Antenna, CustomEmoji, DriveFile, MeDetailed, MessagingMessage, Note, Notification, PageEvent, User, UserGroup } from './entities.js'; - -type FIXME = any; +import { + Antenna, + DriveFile, + DriveFolder, + MeDetailed, + Note, + Notification, + Signin, + User, +} from './autogen/models.js'; +import { + AnnouncementCreated, + EmojiAdded, EmojiDeleted, + EmojiUpdated, + PageEvent, + QueueStats, + QueueStatsLog, + ServerStats, + ServerStatsLog, +} from './entities.js'; export type Channels = { main: { @@ -22,16 +39,11 @@ export type Channels = { readAllUnreadMentions: () => void; unreadSpecifiedNote: (payload: Note['id']) => void; readAllUnreadSpecifiedNotes: () => void; - readAllMessagingMessages: () => void; - messagingMessage: (payload: MessagingMessage) => void; - unreadMessagingMessage: (payload: MessagingMessage) => void; readAllAntennas: () => void; unreadAntenna: (payload: Antenna) => void; readAllAnnouncements: () => void; myTokenRegenerated: () => void; - reversiNoInvites: () => void; - reversiInvited: (payload: FIXME) => void; - signin: (payload: FIXME) => void; + signin: (payload: Signin) => void; registryUpdated: (payload: { scope?: string[]; key: string; @@ -39,32 +51,48 @@ export type Channels = { }) => void; driveFileCreated: (payload: DriveFile) => void; readAntenna: (payload: Antenna) => void; + receiveFollowRequest: (payload: User) => void; + announcementCreated: (payload: AnnouncementCreated) => void; }; receives: null; }; homeTimeline: { - params: null; + params: { + withRenotes?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; localTimeline: { - params: null; + params: { + withRenotes?: boolean; + withReplies?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; hybridTimeline: { - params: null; + params: { + withRenotes?: boolean; + withReplies?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; globalTimeline: { - params: null; + params: { + withRenotes?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; @@ -77,27 +105,69 @@ export type Channels = { }; receives: null; }; - messaging: { + userList: { params: { - otherparty?: User['id'] | null; - group?: UserGroup['id'] | null; + listId: string; + withFiles?: boolean; }; events: { - message: (payload: MessagingMessage) => void; - deleted: (payload: MessagingMessage['id']) => void; - read: (payload: MessagingMessage['id'][]) => void; - typers: (payload: User[]) => void; + note: (payload: Note) => void; }; - receives: { - read: { - id: MessagingMessage['id']; - }; + receives: null; + }; + hashtag: { + params: { + q?: string; + }; + events: { + note: (payload: Note) => void; }; + receives: null; + }; + roleTimeline: { + params: { + roleId: string; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + antenna: { + params: { + antennaId: string; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + channel: { + params: { + channelId: string; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + drive: { + params: null; + events: { + fileCreated: (payload: DriveFile) => void; + fileDeleted: (payload: DriveFile['id']) => void; + fileUpdated: (payload: DriveFile) => void; + folderCreated: (payload: DriveFolder) => void; + folderDeleted: (payload: DriveFolder['id']) => void; + folderUpdated: (payload: DriveFile) => void; + }; + receives: null; }; serverStats: { params: null; events: { - stats: (payload: FIXME) => void; + stats: (payload: ServerStats) => void; + statsLog: (payload: ServerStatsLog) => void; }; receives: { requestLog: { @@ -109,7 +179,8 @@ export type Channels = { queueStats: { params: null; events: { - stats: (payload: FIXME) => void; + stats: (payload: QueueStats) => void; + statsLog: (payload: QueueStatsLog) => void; }; receives: { requestLog: { @@ -118,17 +189,28 @@ export type Channels = { }; }; }; + admin: { + params: null; + events: { + newAbuseUserReport: { + id: string; + targetUserId: string; + reporterId: string; + comment: string; + } + }; + receives: null; + } }; export type NoteUpdatedEvent = { - id: Note['id']; type: 'reacted'; body: { reaction: string; + emoji: string | null; userId: User['id']; }; } | { - id: Note['id']; type: 'unreacted'; body: { reaction: string; @@ -142,13 +224,11 @@ export type NoteUpdatedEvent = { text: string; }; } | { - id: Note['id']; type: 'deleted'; body: { deletedAt: string; }; } | { - id: Note['id']; type: 'pollVoted'; body: { choice: number; @@ -158,7 +238,8 @@ export type NoteUpdatedEvent = { export type BroadcastEvents = { noteUpdated: (payload: NoteUpdatedEvent) => void; - emojiAdded: (payload: { - emoji: CustomEmoji; - }) => void; + emojiAdded: (payload: EmojiAdded) => void; + emojiUpdated: (payload: EmojiUpdated) => void; + emojiDeleted: (payload: EmojiDeleted) => void; + announcementCreated: (payload: AnnouncementCreated) => void; }; diff --git a/packages/misskey-js/test-d/api.ts b/packages/misskey-js/test-d/api.ts index ce793f6fd1..1c2a142e8b 100644 --- a/packages/misskey-js/test-d/api.ts +++ b/packages/misskey-js/test-d/api.ts @@ -8,7 +8,7 @@ describe('API', () => { credential: 'TOKEN' }); const res = await cli.request('meta', { detail: true }); - expectType<Misskey.entities.DetailedInstanceMetadata>(res); + expectType<Misskey.entities.MetaResponse>(res); }); test('conditional respose type (meta)', async () => { @@ -18,16 +18,16 @@ describe('API', () => { }); const res = await cli.request('meta', { detail: true }); - expectType<Misskey.entities.DetailedInstanceMetadata>(res); + expectType<Misskey.entities.MetaResponse>(res); const res2 = await cli.request('meta', { detail: false }); - expectType<Misskey.entities.LiteInstanceMetadata>(res2); + expectType<Misskey.entities.MetaResponse>(res2); const res3 = await cli.request('meta', { }); - expectType<Misskey.entities.LiteInstanceMetadata>(res3); + expectType<Misskey.entities.MetaResponse>(res3); const res4 = await cli.request('meta', { detail: true as boolean }); - expectType<Misskey.entities.LiteInstanceMetadata | Misskey.entities.DetailedInstanceMetadata>(res4); + expectType<Misskey.entities.MetaResponse>(res4); }); test('conditional respose type (users/show)', async () => { diff --git a/packages/misskey-js/test-d/streaming.ts b/packages/misskey-js/test-d/streaming.ts index 853f376e6c..6b186bd45a 100644 --- a/packages/misskey-js/test-d/streaming.ts +++ b/packages/misskey-js/test-d/streaming.ts @@ -9,18 +9,4 @@ describe('Streaming', () => { expectType<Misskey.entities.Notification>(notification); }); }); - - test('params type', async () => { - const stream = new Misskey.Stream('https://misskey.test', { token: 'TOKEN' }); - // TODO: 「stream.useChannel の第二引数として受け入れる型が - // { - // otherparty?: User['id'] | null; - // group?: UserGroup['id'] | null; - // } - // になっている」というテストを行いたいけどtsdでの書き方がわからない - const messagingChannel = stream.useChannel('messaging', { otherparty: 'aaa' }); - messagingChannel.on('message', message => { - expectType<Misskey.entities.MessagingMessage>(message); - }); - }); }); diff --git a/packages/misskey-js/tsconfig.json b/packages/misskey-js/tsconfig.json index e2b755075b..f56b65e868 100644 --- a/packages/misskey-js/tsconfig.json +++ b/packages/misskey-js/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2022", "module": "nodenext", diff --git a/packages/sw/package.json b/packages/sw/package.json index 3259cae879..c48efd6ea6 100644 --- a/packages/sw/package.json +++ b/packages/sw/package.json @@ -2,23 +2,24 @@ "name": "sw", "private": true, "scripts": { - "watch": "node build.js watch", + "watch": "nodemon -w ../../package.json -e json --exec \"node build.js watch\"", "build": "node build.js", "typecheck": "tsc --noEmit", "eslint": "eslint --quiet src/**/*.ts", "lint": "pnpm typecheck && pnpm eslint" }, "dependencies": { - "esbuild": "0.19.5", + "esbuild": "0.19.9", "idb-keyval": "6.2.1", "misskey-js": "workspace:*" }, "devDependencies": { - "@typescript-eslint/parser": "6.11.0", + "@typescript-eslint/parser": "6.14.0", "@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67", - "eslint": "8.53.0", - "eslint-plugin-import": "2.29.0", - "typescript": "5.2.2" + "eslint": "8.56.0", + "eslint-plugin-import": "2.29.1", + "nodemon": "3.0.2", + "typescript": "5.3.3" }, "type": "module" } |