From 99446d440ee642e41d166854442d2ce8c99938f8 Mon Sep 17 00:00:00 2001 From: Chocolate Pie <106949016+chocolate-pie@users.noreply.github.com> Date: Mon, 5 Jun 2023 08:10:15 +0900 Subject: [PATCH 01/10] =?UTF-8?q?Api=20Token=E3=82=92=E8=A6=81=E6=B1=82?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E9=96=A2=E6=95=B0=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=20(misskey-dev/misskey#10911)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 3 + locales/ja-JP.yml | 3 + packages/backend/src/core/CacheService.ts | 9 +++ packages/backend/src/misc/flash-token.ts | 6 ++ .../backend/src/server/api/ApiCallService.ts | 20 +++++-- .../src/server/api/AuthenticateService.ts | 18 ++++-- .../backend/src/server/api/EndpointsModule.ts | 4 ++ packages/backend/src/server/api/endpoints.ts | 2 + .../server/api/endpoints/flash/gen-token.ts | 56 ++++++++++++++++++ .../server/api/endpoints/i/2fa/key-done.ts | 2 +- .../components/MkFlashRequestTokenDialog.vue | 58 +++++++++++++++++++ packages/frontend/src/local-storage.ts | 1 + packages/frontend/src/scripts/aiscript/api.ts | 35 ++++++++++- packages/misskey-js/src/api.types.ts | 2 + 14 files changed, 206 insertions(+), 13 deletions(-) create mode 100644 packages/backend/src/misc/flash-token.ts create mode 100644 packages/backend/src/server/api/endpoints/flash/gen-token.ts create mode 100644 packages/frontend/src/components/MkFlashRequestTokenDialog.vue diff --git a/locales/index.d.ts b/locales/index.d.ts index a54268676e0..8d9dbdc594f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1063,6 +1063,9 @@ export interface Locale { "changeReactionConfirm": string; "later": string; "goToMisskey": string; + "additionalPermissionsForFlash": string; + "thisFlashRequiresTheFollowingPermissions": string; + "doYouWantToAllowThisPlayToAccessYourAccount": string; "_initialAccountSetting": { "accountCreated": string; "letsStartAccountSetup": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0ca37caa582..e34447a40fd 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1060,6 +1060,9 @@ cancelReactionConfirm: "リアクションを取り消しますか?" changeReactionConfirm: "リアクションを変更しますか?" later: "あとで" goToMisskey: "Misskeyへ" +additionalPermissionsForFlash: "プレイへの追加許可" +thisFlashRequiresTheFollowingPermissions: "このプレイは以下の権限を要求しています" +doYouWantToAllowThisPlayToAccessYourAccount: "このプレイによるアカウントへのアクセスを許可しますか?" _initialAccountSetting: accountCreated: "アカウントの作成が完了しました!" diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index de33e4c243d..3d78015a532 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -7,6 +7,7 @@ import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { StreamMessages } from '@/server/api/stream/types.js'; +import type { FlashToken } from '@/misc/flash-token'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() @@ -16,6 +17,7 @@ export class CacheService implements OnApplicationShutdown { public localUserByIdCache: MemoryKVCache; public uriPersonCache: MemoryKVCache; public userProfileCache: RedisKVCache; + public flashAccessTokensCache: RedisKVCache; public userMutingsCache: RedisKVCache>; public userBlockingCache: RedisKVCache>; public userBlockedCache: RedisKVCache>; // NOTE: 「被」Blockキャッシュ @@ -116,6 +118,13 @@ export class CacheService implements OnApplicationShutdown { fromRedisConverter: (value) => new Set(JSON.parse(value)), }); + this.flashAccessTokensCache = new RedisKVCache(this.redisClient, 'flashAccessTokens', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: async (key) => null, + toRedisConverter: (value) => JSON.stringify(value), + fromRedisConverter: (value) => JSON.parse(value), + }); this.redisForSub.on('message', this.onMessage); } diff --git a/packages/backend/src/misc/flash-token.ts b/packages/backend/src/misc/flash-token.ts new file mode 100644 index 00000000000..2622f955dfc --- /dev/null +++ b/packages/backend/src/misc/flash-token.ts @@ -0,0 +1,6 @@ +import type { LocalUser } from '@/models/entities/User.js'; + +export type FlashToken = { + permissions: string[]; + user: LocalUser +}; diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index dad1a4132a9..e94259d014c 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -20,6 +20,7 @@ import { AuthenticateService, AuthenticationError } from './AuthenticateService. import type { FastifyRequest, FastifyReply } from 'fastify'; import type { OnApplicationShutdown } from '@nestjs/common'; import type { IEndpointMeta, IEndpoint } from './endpoints.js'; +import type { FlashToken } from '@/misc/flash-token.js'; const pump = promisify(pipeline); @@ -68,8 +69,8 @@ export class ApiCallService implements OnApplicationShutdown { reply.code(400); return; } - this.authenticateService.authenticate(token).then(([user, app]) => { - this.call(endpoint, user, app, body, null, request).then((res) => { + this.authenticateService.authenticate(token).then(([user, app, flashToken]) => { + this.call(endpoint, user, app, flashToken, body, null, request).then((res) => { if (request.method === 'GET' && endpoint.meta.cacheSec && !body?.['i'] && !user) { reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`); } @@ -122,8 +123,8 @@ export class ApiCallService implements OnApplicationShutdown { reply.code(400); return; } - this.authenticateService.authenticate(token).then(([user, app]) => { - this.call(endpoint, user, app, fields, { + this.authenticateService.authenticate(token).then(([user, app, flashToken]) => { + this.call(endpoint, user, app, flashToken, fields, { name: multipartData.filename, path: path, }, request).then((res) => { @@ -199,6 +200,7 @@ export class ApiCallService implements OnApplicationShutdown { ep: IEndpoint & { exec: any }, user: LocalUser | null | undefined, token: AccessToken | null | undefined, + flashToken: FlashToken | null | undefined, data: any, file: { name: string; @@ -206,7 +208,7 @@ export class ApiCallService implements OnApplicationShutdown { } | null, request: FastifyRequest<{ Body: Record | undefined, Querystring: Record }>, ) { - const isSecure = user != null && token == null; + const isSecure = user != null && token == null && flashToken == null; if (ep.meta.secure && !isSecure) { throw new ApiError(accessDenied); @@ -309,6 +311,14 @@ export class ApiCallService implements OnApplicationShutdown { }); } + if (flashToken && ep.meta.kind && !flashToken.permissions.some(p => p === ep.meta.kind)) { + throw new ApiError({ + message: 'Your flash does not have the necessary permissions to use this endpoint.', + code: 'PERMISSION_DENIED', + id: '11924d17-113a-4ab0-954a-c567ee8a6ce5', + }); + } + // Cast non JSON input if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) { for (const k of Object.keys(ep.params.properties)) { diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index e23591d8765..e7a31477e23 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -8,6 +8,7 @@ import type { App } from '@/models/entities/App.js'; import { CacheService } from '@/core/CacheService.js'; import isNativeToken from '@/misc/is-native-token.js'; import { bindThis } from '@/decorators.js'; +import type { FlashToken } from '@/misc/flash-token'; export class AuthenticationError extends Error { constructor(message: string) { @@ -36,9 +37,9 @@ export class AuthenticateService { } @bindThis - public async authenticate(token: string | null | undefined): Promise<[LocalUser | null, AccessToken | null]> { + public async authenticate(token: string | null | undefined): Promise<[LocalUser | null, AccessToken | null, FlashToken | null]> { if (token == null) { - return [null, null]; + return [null, null, null]; } if (isNativeToken(token)) { @@ -49,7 +50,7 @@ export class AuthenticateService { throw new AuthenticationError('user not found'); } - return [user, null]; + return [user, null, null]; } else { const accessToken = await this.accessTokensRepository.findOne({ where: [{ @@ -60,7 +61,12 @@ export class AuthenticateService { }); if (accessToken == null) { - throw new AuthenticationError('invalid signature'); + const flashToken = await this.cacheService.flashAccessTokensCache.get(token); + if (flashToken !== null && typeof flashToken !== 'undefined') { + return [flashToken.user, null, flashToken]; + } else { + throw new AuthenticationError('invalid signature'); + } } this.accessTokensRepository.update(accessToken.id, { @@ -79,9 +85,9 @@ export class AuthenticateService { return [user, { id: accessToken.id, permission: app.permission, - } as AccessToken]; + } as AccessToken, null]; } else { - return [user, accessToken]; + return [user, accessToken, null]; } } } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 1e32e9988df..ca20ff84fec 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -284,6 +284,7 @@ import * as ep___pages_update from './endpoints/pages/update.js'; import * as ep___flash_create from './endpoints/flash/create.js'; import * as ep___flash_delete from './endpoints/flash/delete.js'; import * as ep___flash_featured from './endpoints/flash/featured.js'; +import * as ep___flash_genToken from './endpoints/flash/gen-token.js'; import * as ep___flash_like from './endpoints/flash/like.js'; import * as ep___flash_show from './endpoints/flash/show.js'; import * as ep___flash_unlike from './endpoints/flash/unlike.js'; @@ -625,6 +626,7 @@ const $pages_update: Provider = { provide: 'ep:pages/update', useClass: ep___pag const $flash_create: Provider = { provide: 'ep:flash/create', useClass: ep___flash_create.default }; const $flash_delete: Provider = { provide: 'ep:flash/delete', useClass: ep___flash_delete.default }; const $flash_featured: Provider = { provide: 'ep:flash/featured', useClass: ep___flash_featured.default }; +const $flash_genToken: Provider = { provide: 'ep:flash/gen-token', useClass: ep___flash_genToken.default }; const $flash_like: Provider = { provide: 'ep:flash/like', useClass: ep___flash_like.default }; const $flash_show: Provider = { provide: 'ep:flash/show', useClass: ep___flash_show.default }; const $flash_unlike: Provider = { provide: 'ep:flash/unlike', useClass: ep___flash_unlike.default }; @@ -970,6 +972,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $flash_create, $flash_delete, $flash_featured, + $flash_genToken, $flash_like, $flash_show, $flash_unlike, @@ -1309,6 +1312,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $flash_create, $flash_delete, $flash_featured, + $flash_genToken, $flash_like, $flash_show, $flash_unlike, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 7e678a64040..bf8542f9600 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -284,6 +284,7 @@ import * as ep___pages_update from './endpoints/pages/update.js'; import * as ep___flash_create from './endpoints/flash/create.js'; import * as ep___flash_delete from './endpoints/flash/delete.js'; import * as ep___flash_featured from './endpoints/flash/featured.js'; +import * as ep___flash_genToken from './endpoints/flash/gen-token.js'; import * as ep___flash_like from './endpoints/flash/like.js'; import * as ep___flash_show from './endpoints/flash/show.js'; import * as ep___flash_unlike from './endpoints/flash/unlike.js'; @@ -623,6 +624,7 @@ const eps = [ ['flash/create', ep___flash_create], ['flash/delete', ep___flash_delete], ['flash/featured', ep___flash_featured], + ['flash/gen-token', ep___flash_genToken], ['flash/like', ep___flash_like], ['flash/show', ep___flash_show], ['flash/unlike', ep___flash_unlike], diff --git a/packages/backend/src/server/api/endpoints/flash/gen-token.ts b/packages/backend/src/server/api/endpoints/flash/gen-token.ts new file mode 100644 index 00000000000..bcbce360f17 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/flash/gen-token.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { CacheService } from '@/core/CacheService.js'; + +export const meta = { + tags: ['flash'], + + requireCredential: true, + + prohibitMoved: true, + + secure: true, + + limit: { + duration: ms('1hour'), + max: 30, + }, + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + token: { type: 'string' }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + permissions: { type: 'array', items: { + type: 'string', + } }, + }, + required: ['permissions'], +} as const; + +@Injectable() // eslint-disable-next-line import/no-default-export +export default class extends Endpoint { + constructor ( + private cacheService: CacheService, + ) { + super(meta, paramDef, async (ps, me) => { + const token = secureRndstr(32, true); + await this.cacheService.flashAccessTokensCache.set(token, { + user: me, + permissions: ps.permissions, + }); + return { + token, + }; + }); + } +} 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 ad33398da60..e8985a9cd83 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 @@ -1,6 +1,6 @@ import { promisify } from 'node:util'; import bcrypt from 'bcryptjs'; -import * as cbor from 'cbor'; +import cbor from 'cbor'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; diff --git a/packages/frontend/src/components/MkFlashRequestTokenDialog.vue b/packages/frontend/src/components/MkFlashRequestTokenDialog.vue new file mode 100644 index 00000000000..9f65f211685 --- /dev/null +++ b/packages/frontend/src/components/MkFlashRequestTokenDialog.vue @@ -0,0 +1,58 @@ + + + diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 441a35747a3..242f456f1cb 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -29,6 +29,7 @@ type Keys = `ui:folder:${string}` | `themes:${string}` | `aiscript:${string}` | + `aiscriptSecure:${string}` | 'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~) 'emojis' // DEPRECATED, stored in indexeddb (13.9.0~); diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index b6b7445b672..76132404283 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -1,4 +1,6 @@ import { utils, values } from '@syuilo/aiscript'; +import { defineAsyncComponent } from 'vue'; +import { permissions as MkPermissions } from 'misskey-js'; import * as os from '@/os'; import { $i } from '@/account'; import { miLocalStorage } from '@/local-storage'; @@ -6,6 +8,10 @@ import { customEmojis } from '@/custom-emojis'; export function createAiScriptEnv(opts) { let apiRequests = 0; + const table = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + const randomString = Array.from(crypto.getRandomValues(new Uint32Array(32))) + .map(v => table[v % table.length]) + .join(''); return { USER_ID: $i ? values.STR($i.id) : values.NULL, USER_NAME: $i ? values.STR($i.name) : values.NULL, @@ -35,7 +41,7 @@ export function createAiScriptEnv(opts) { } apiRequests++; if (apiRequests > 16) return values.NULL; - const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token ?? null)); + const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : miLocalStorage.getItem(`aiscriptSecure:${opts.storageKey}:${randomString}:accessToken`) ?? (opts.token ?? null)); return utils.jsToVal(res); }), 'Mk:save': values.FN_NATIVE(([key, value]) => { @@ -47,5 +53,32 @@ export function createAiScriptEnv(opts) { utils.assertString(key); return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`))); }), + 'Mk:requestToken': values.FN_NATIVE(async ([ value ]) => { + utils.assertArray(value); + const permissions = (utils.valToJs(value) as unknown[]).map(val => { + if (typeof val !== 'string') { + throw new Error(`Invalid type. expected string but got ${typeof val}`); + } + return val; + }).filter(val => MkPermissions.includes(val)); + return await new Promise(async (resolve: any) => { + await os.popup(defineAsyncComponent(() => import('@/components/MkFlashRequestTokenDialog.vue')), { + permissions, + }, { + accept: () => { + os.api('flash/gen-token', { + permissions, + }).then(res => { + miLocalStorage.setItem(`aiscriptSecure:${opts.storageKey}:${randomString}:accessToken`, res!.token); + resolve(values.TRUE); + }); + }, + cancel: () => resolve(values.FALSE), + closed: () => { + resolve(values.FALSE); + }, + }, 'closed'); + }); + }), }; } diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index cc88c4b1a40..566d17716ce 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -431,6 +431,8 @@ export type Endpoints = { 'i/2fa/remove-key': { req: TODO; res: TODO; }; 'i/2fa/unregister': { req: TODO; res: TODO; }; + // flash + 'flash/gen-token': { req: TODO; res: TODO; }; // 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[]; }; From 694a6a302c40a4f9a870349ccc47fd01d6b29991 Mon Sep 17 00:00:00 2001 From: Chocolate Pie <106949016+chocolate-pie@users.noreply.github.com> Date: Mon, 5 Jun 2023 09:03:58 +0900 Subject: [PATCH 02/10] :sparkles: --- CHANGELOG.md | 1 + packages/frontend/src/scripts/aiscript/api.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb8ab8f30f5..530118e7211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - タイムラインにフォロイーの行った他人へのリプライを含めるかどうかの設定をアカウントに保存するのをやめるように - 今後はAPI呼び出し時およびストリーミング接続時に設定するようになります - リストを公開できるようになりました +- プレイにAPI Tokenを要求できる関数を追加 ### Client - リアクションの取り消し/変更時に確認ダイアログを出すように diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 76132404283..64a5f41b3a4 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -53,7 +53,7 @@ export function createAiScriptEnv(opts) { utils.assertString(key); return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`))); }), - 'Mk:requestToken': values.FN_NATIVE(async ([ value ]) => { + 'Mk:requestToken': values.FN_NATIVE(async ([value]) => { utils.assertArray(value); const permissions = (utils.valToJs(value) as unknown[]).map(val => { if (typeof val !== 'string') { From 08b6a2008a0dd462314a81ea6fd4f39c10af6ce2 Mon Sep 17 00:00:00 2001 From: Chocolate Pie <106949016+chocolate-pie@users.noreply.github.com> Date: Tue, 6 Jun 2023 07:54:27 +0900 Subject: [PATCH 03/10] Fix translate --- locales/ja-JP.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 4987c477145..a4d779bb200 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1062,9 +1062,9 @@ later: "あとで" goToMisskey: "Misskeyへ" additionalEmojiDictionary: "絵文字の追加辞書" installed: "インストール済み" -additionalPermissionsForFlash: "プレイへの追加許可" -thisFlashRequiresTheFollowingPermissions: "このプレイは以下の権限を要求しています" -doYouWantToAllowThisPlayToAccessYourAccount: "このプレイによるアカウントへのアクセスを許可しますか?" +additionalPermissionsForFlash: "Playへの追加許可" +thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求しています" +doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか?" _initialAccountSetting: accountCreated: "アカウントの作成が完了しました!" From 7b63146d98324735e6a57ddeb4e856fc1df87e75 Mon Sep 17 00:00:00 2001 From: Chocolate Pie <106949016+chocolate-pie@users.noreply.github.com> Date: Wed, 7 Jun 2023 11:00:56 +0900 Subject: [PATCH 04/10] Fix Vulnerability --- packages/backend/src/server/api/ApiCallService.ts | 2 +- packages/backend/src/server/api/endpoint-base.ts | 9 +++++---- packages/backend/src/server/api/endpoints/app/show.ts | 4 ++-- .../src/server/api/endpoints/drive/files/create.ts | 2 +- .../server/api/endpoints/drive/files/upload-from-url.ts | 2 +- packages/backend/src/server/api/endpoints/i.ts | 4 ++-- packages/backend/src/server/api/endpoints/i/update.ts | 4 ++-- packages/backend/src/server/api/endpoints/users/show.ts | 2 +- 8 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index e94259d014c..a9d750f3514 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -341,7 +341,7 @@ export class ApiCallService implements OnApplicationShutdown { } // API invoking - return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => { + return await ep.exec(data, user, token, flashToken, file, request.ip, request.headers).catch((err: Error) => { if (err instanceof ApiError || err instanceof AuthenticationError) { throw err; } else { diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts index 1555a3ca468..5aae602c004 100644 --- a/packages/backend/src/server/api/endpoint-base.ts +++ b/packages/backend/src/server/api/endpoint-base.ts @@ -3,6 +3,7 @@ import Ajv from 'ajv'; import type { Schema, SchemaType } from '@/misc/json-schema.js'; import type { LocalUser } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; +import type { FlashToken } from '@/misc/flash-token.js'; import { ApiError } from './error.js'; import type { IEndpointMeta } from './endpoints.js'; @@ -21,16 +22,16 @@ type File = { // TODO: paramsの型をT['params']のスキーマ定義から推論する type Executor = - (params: SchemaType, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record | null) => + (params: SchemaType, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, flashToken: FlashToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record | null) => Promise>>; export abstract class Endpoint { - public exec: (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => Promise; + public exec: (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, flashToken: FlashToken | null, file?: File, ip?: string | null, headers?: Record | null) => Promise; constructor(meta: T, paramDef: Ps, cb: Executor) { const validate = ajv.compile(paramDef); - this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => { + this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, flashToken: FlashToken | null, file?: File, ip?: string | null, headers?: Record | null) => { let cleanup: undefined | (() => void) = undefined; if (meta.requireFile) { @@ -61,7 +62,7 @@ export abstract class Endpoint { return Promise.reject(err); } - return cb(params as SchemaType, user, token, file, cleanup, ip, headers); + return cb(params as SchemaType, user, token, flashToken, file, cleanup, ip, headers); }; } } diff --git a/packages/backend/src/server/api/endpoints/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts index eaafa8dc1b0..52e9c02aba7 100644 --- a/packages/backend/src/server/api/endpoints/app/show.ts +++ b/packages/backend/src/server/api/endpoints/app/show.ts @@ -40,8 +40,8 @@ export default class extends Endpoint { private appEntityService: AppEntityService, ) { - super(meta, paramDef, async (ps, user, token) => { - const isSecure = user != null && token == null; + super(meta, paramDef, async (ps, user, token, flashToken) => { + const isSecure = user != null && token == null && flashToken == null; // Lookup app const ap = await this.appsRepository.findOneBy({ id: ps.appId }); diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index a1c1f9325ec..e128e80944a 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -78,7 +78,7 @@ export default class extends Endpoint { private metaService: MetaService, private driveService: DriveService, ) { - super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => { + super(meta, paramDef, async (ps, me, _1, _2, file, cleanup, ip, headers) => { // Get 'name' parameter let name = ps.name ?? file!.name ?? null; if (name != null) { diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index c835587c4aa..4edc4cdc9fe 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -48,7 +48,7 @@ export default class extends Endpoint { private driveService: DriveService, private globalEventService: GlobalEventService, ) { - super(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => { + super(meta, paramDef, async (ps, user, _1, _2, _3, _4, ip, headers) => { this.driveService.uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => { this.driveFileEntityService.pack(file, { self: true }).then(packedFile => { this.globalEventService.publishMainStream(user.id, 'urlUploadFinished', { diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index a3e3e02a124..f336caeb771 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -44,8 +44,8 @@ export default class extends Endpoint { private userEntityService: UserEntityService, ) { - super(meta, paramDef, async (ps, user, token) => { - const isSecure = token == null; + super(meta, paramDef, async (ps, user, token, flashToken) => { + const isSecure = token == null && flashToken == null; const now = new Date(); const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`; diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 8f5e6177c28..5f4bc8cf3f9 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -194,9 +194,9 @@ export default class extends Endpoint { private roleService: RoleService, private cacheService: CacheService, ) { - super(meta, paramDef, async (ps, _user, token) => { + super(meta, paramDef, async (ps, _user, token, flashToken) => { const user = await this.usersRepository.findOneByOrFail({ id: _user.id }); - const isSecure = token == null; + const isSecure = token == null && flashToken == null; const updates = {} as Partial; const profileUpdates = {} as Partial; diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index ba432c273bf..ed42c66de86 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -87,7 +87,7 @@ export default class extends Endpoint { private perUserPvChart: PerUserPvChart, private apiLoggerService: ApiLoggerService, ) { - super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => { + super(meta, paramDef, async (ps, me, _1, _2, _3, _4, ip) => { let user; const isModerator = await this.roleService.isModerator(me); From 1ebe572d2afd376ca80356a55e8132b8b5539ddc Mon Sep 17 00:00:00 2001 From: Chocolate Pie <106949016+chocolate-pie@users.noreply.github.com> Date: Wed, 7 Jun 2023 11:55:05 +0900 Subject: [PATCH 05/10] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c88375b65a1..701eec5dd57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,13 @@ --> -## 13.13.1 +## 13.13.2 (unreleased) ### General - プレイにAPI Tokenを要求できる関数を追加 +## 13.13.1 + ### Client - Fix: タブがアクティブな間はstreamが切断されないように From e045aa2661f3968184069237ef5deeba6d1474dd Mon Sep 17 00:00:00 2001 From: Chocolate Pie <106949016+chocolate-pie@users.noreply.github.com> Date: Sun, 11 Jun 2023 15:50:38 +0900 Subject: [PATCH 06/10] Merge remote-tracking branch 'upstream/develop' into flash-request-token --- CHANGELOG.md | 14 ++-- locales/de-DE.yml | 11 +-- locales/en-US.yml | 11 +-- locales/ja-JP.yml | 8 +-- locales/tr-TR.yml | 71 +++++++++++++++++++ locales/zh-CN.yml | 1 + locales/zh-TW.yml | 1 + package.json | 2 +- packages/backend/src/core/CacheService.ts | 11 +++ .../backend/src/core/CustomEmojiService.ts | 14 +++- .../src/core/FederatedInstanceService.ts | 14 +++- .../src/core/PushNotificationService.ts | 14 +++- packages/backend/src/core/RoleService.ts | 1 + .../backend/src/core/UserKeypairService.ts | 14 +++- .../core/activitypub/ApDbResolverService.ts | 15 +++- packages/backend/src/misc/cache.ts | 32 ++++++++- .../src/server/api/AuthenticateService.ts | 14 +++- .../src/server/api/endpoints/roles/list.ts | 1 + .../src/server/api/endpoints/roles/users.ts | 1 + .../src/components/global/MkError.vue | 4 +- packages/frontend/src/ui/deck/column.vue | 1 + packages/misskey-js/etc/misskey-js.api.md | 4 +- 22 files changed, 223 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a054d33da5..b4c24b0edc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,20 +11,24 @@ - --> - -## 13.13.2 (unreleased) +## 13.13.2 ### General +- エラー時や項目が存在しないときなどのアイコン画像をサーバー管理者が設定できるように +- ロールが付与されているユーザーリストを非公開にできるように - プレイにAPI Tokenを要求できる関数を追加 +### Client +- Fix: タブがバックグラウンドでもstreamが切断されないように + +### Server +- Fix: キャッシュが溜まり続けないように + ## 13.13.1 ### Client - Fix: タブがアクティブな間はstreamが切断されないように -### General -- エラー時や項目が存在しないときなどのアイコン画像をサーバー管理者が設定できるようになりました - ### Server - Fix: api/metaで`TypeError: JSON5.parse is not a function`エラーが発生する問題を修正 diff --git a/locales/de-DE.yml b/locales/de-DE.yml index c4c12cb1aa8..9b52a506974 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -991,7 +991,7 @@ postToTheChannel: "In Kanal senden" cannotBeChangedLater: "Kann später nicht mehr geändert werden." reactionAcceptance: "Reaktionsannahme" likeOnly: "Nur \"Gefällt mir\"" -likeOnlyForRemote: "Nur \"Gefällt mir\" für fremde Instanzen" +likeOnlyForRemote: "Alle (Nur \"Gefällt mir\" für fremde Instanzen)" nonSensitiveOnly: "Keine Sensitiven" nonSensitiveOnlyForLocalLikeOnlyForRemote: "Keine Sensitiven (Nur \"Gefällt mir\" von fremden Instanzen)" rolesAssignedToMe: "Mir zugewiesene Rollen" @@ -1062,6 +1062,7 @@ later: "Später" goToMisskey: "Zu Misskey" additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher" installed: "Installiert" +branding: "Branding" _initialAccountSetting: accountCreated: "Dein Konto wurde erfolgreich erstellt!" letsStartAccountSetup: "Lass uns nun dein Konto einrichten." @@ -1093,7 +1094,7 @@ _accountMigration: migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden." movedAndCannotBeUndone: "\nDieses Konto wurde migriert.\nDiese Aktion ist unwiderruflich." postMigrationNote: "Dieses Konto wird 24 Stunden nach Abschluss der Migration allen Konten, denen es derzeit folgt, nicht mehr folgen.\n\nSowohl die Anzahl der Follower als auch die der Konten, denen dieses Konto folgt, wird dann auf Null gesetzt. Um zu vermeiden, dass Follower dieses Kontos dessen Beiträge, welche nur für Follower bestimmt sind, nicht mehr sehen können, werden sie diesem Konto jedoch weiterhin folgen." - movedTo: "Umzugsziel:" + movedTo: "Neues Konto:" _achievements: earnedAt: "Freigeschaltet am" _types: @@ -1347,7 +1348,7 @@ _role: condition: "Bedingung" isConditionalRole: "Dies ist eine konditionale Rolle." isPublic: "Öffentliche Rolle" - descriptionOfIsPublic: "Ist dies aktiviert, so kann jeder die Liste der Benutzer, die dieser Rolle zugewiesen sind, einsehen. Zusätzlich wird diese Rolle im Profil zugewiesener Benutzer angezeigt." + descriptionOfIsPublic: "Diese Rolle wird im Profil zugewiesener Benutzer angezeigt." options: "Optionen" policies: "Richtlinien" baseRole: "Rollenvorlage" @@ -1356,8 +1357,8 @@ _role: iconUrl: "Icon-URL" asBadge: "Als Abzeichen anzeigen" descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt." - isExplorable: "Rollenchronik veröffentlichen" - descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Rollenchronik dieser Rolle frei zugänglich. Die Chronik von Rollen, welche nicht öffentlich sind, wird auch bei Aktivierung nicht veröffentlicht." + isExplorable: "Benutzerliste veröffentlichen" + descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Chronik dieser Rolle, sowie eine Liste der Benutzer mit dieser Rolle, frei zugänglich." displayOrder: "Position" descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position." canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" diff --git a/locales/en-US.yml b/locales/en-US.yml index 0f1c7c89fee..8938208574e 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -991,7 +991,7 @@ postToTheChannel: "Post to channel" cannotBeChangedLater: "This cannot be changed later." reactionAcceptance: "Reaction Acceptance" likeOnly: "Only likes" -likeOnlyForRemote: "Only likes for remote instances" +likeOnlyForRemote: "All (Only likes for remote instances)" nonSensitiveOnly: "Non-sensitive only" nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)" rolesAssignedToMe: "Roles assigned to me" @@ -1062,6 +1062,7 @@ later: "Later" goToMisskey: "To Misskey" additionalEmojiDictionary: "Additional emoji dictionaries" installed: "Installed" +branding: "Branding" _initialAccountSetting: accountCreated: "Your account was successfully created!" letsStartAccountSetup: "For starters, let's set up your profile." @@ -1093,7 +1094,7 @@ _accountMigration: migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore." movedAndCannotBeUndone: "\nThis account has been migrated.\nMigration cannot be reversed." postMigrationNote: "This account will unfollow all accounts it is currently following 24 hours after migration finishes.\nBoth the number of follows and followers will then become zero. To avoid your followers from being unable to see followers only posts of this account, they will however continue following this account." - movedTo: "Account to move to:" + movedTo: "New account:" _achievements: earnedAt: "Unlocked at" _types: @@ -1347,7 +1348,7 @@ _role: condition: "Condition" isConditionalRole: "This is a conditional role." isPublic: "Public role" - descriptionOfIsPublic: "Anyone will be able to view a list of users assigned to this role. In addition, this role will be displayed in the profiles of assigned users." + descriptionOfIsPublic: "This role will be displayed in the profiles of assigned users." options: "Options" policies: "Policies" baseRole: "Role template" @@ -1356,8 +1357,8 @@ _role: iconUrl: "Icon URL" asBadge: "Show as badge" descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on." - isExplorable: "Role timeline is public" - descriptionOfIsExplorable: "This role's timeline will become publicly accessible if enabled. Timelines of non-public roles will not be made public even if set." + isExplorable: "Make role explorable" + descriptionOfIsExplorable: "This role's timeline and the list of users with this will be made public if enabled." displayOrder: "Position" descriptionOfDisplayOrder: "The higher the number, the higher its UI position." canEditMembersByModerator: "Allow moderators to edit the list of members for this role" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 16f90c4d036..efdb95fafc4 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1355,8 +1355,8 @@ _role: conditional: "コンディショナル" condition: "条件" isConditionalRole: "これはコンディショナルロールです。" - isPublic: "ロールを公開" - descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができます。また、ユーザーのプロフィールでこのロールが表示されます。" + isPublic: "公開ロール" + descriptionOfIsPublic: "ユーザーのプロフィールでこのロールが表示されます。" options: "オプション" policies: "ポリシー" baseRole: "ベースロール" @@ -1365,8 +1365,8 @@ _role: iconUrl: "アイコン画像のURL" asBadge: "バッジとして表示" descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。" - isExplorable: "ロールタイムラインを公開" - descriptionOfIsExplorable: "オンにすると、ロールのタイムラインを公開します。ロールの公開がオフの場合、タイムラインの公開はされません。" + isExplorable: "ユーザーを見つけやすくする" + descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。" displayOrder: "表示順" descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。" canEditMembersByModerator: "モデレーターのメンバー編集を許可" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 7bd8188a48f..cc402eec484 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -1,6 +1,7 @@ --- _lang_: "Türkçe" introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀." +poweredByMisskeyDescription: "name}Açık kaynak bir platform\nMisskeyDünya'nın en sunucularında biri。" monthAndDay: "{month}Ay {day}Gün" search: "Arama" notifications: "Bildirim" @@ -13,7 +14,9 @@ cancel: "İptal" enterUsername: "Kullanıcı adınızı giriniz" noNotes: "Notlar mevcut değil." noNotifications: "Bildirim bulunmuyor" +instance: "Sunucu" settings: "Ayarlar" +notificationSettings: "Bildirim Ayarları" basicSettings: "Temel Ayarlar" otherSettings: "Diğer Ayarlar" openInWindow: "Bir pencere ile aç" @@ -21,9 +24,11 @@ profile: "Profil" timeline: "Zaman çizelgesi" noAccountDescription: "Bu kullanıcı henüz biyografisini yazmadı" login: "Giriş Yap " +loggingIn: "Oturum aç" logout: "Çıkış Yap" signup: "Kayıt Ol" uploading: "Yükleniyor" +save: "Kaydet" users: "Kullanıcı" addUser: "Kullanıcı Ekle" favorite: "Favoriler" @@ -31,6 +36,7 @@ favorites: "Favoriler" unfavorite: "Favorilerden Kaldır" favorited: "Favorilerime eklendi." alreadyFavorited: "Zaten favorilerinizde kayıtlı." +cantFavorite: "Favorilere kayıt yapılamadı" pin: "Sabitlenmiş" unpin: "Sabitlemeyi kaldır" copyContent: "İçeriği kopyala" @@ -40,23 +46,88 @@ deleteAndEdit: "Sil ve yeniden düzenle" deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir." addToList: "Listeye ekle" sendMessage: "Mesaj Gönder" +copyRSS: "RSSKopyala" copyUsername: "Kullanıcı Adını Kopyala" +copyUserId: "KullanıcıyıKopyala" +copyNoteId: "Kimlik notunu kopyala" searchUser: "Kullanıcıları ara" +reply: "yanıt" +loadMore: "Devamını yükle" +showMore: "Devamını yükle" +lists: "Listeler" +noLists: "Liste yok" +note: "not" +notes: "notlar" +following: "takipçi" +followers: "takipçi" +followsYou: "seni takip ediyor" +createList: "Liste oluştur" +manageLists: "Yönetici Listeleri" +error: "hata" +follow: "takipçi" +followRequest: "Takip isteği" +followRequests: "Takip istekleri" +unfollow: "takip etmeyi bırak" +followRequestPending: "Bekleyen Takip Etme Talebi" +enterEmoji: "Emoji Giriniz" +renote: "vazgeçme" +unrenote: "not alma" +renoted: "yeniden adlandırılmış" +cantRenote: "Ayrılamama" +cantReRenote: "not alabilirmiyim" +quote: "alıntı" +pinnedNote: "Sabitlenen" pinned: "Sabitlenmiş" +you: "sen" +unmute: "sesi aç" +renoteMute: "sesi kapat" +renoteUnmute: "sesi açmayı iptal et" +block: "engelle" +unblock: "engellemeyi kaldır" +suspend: "askıya al" +unsuspend: "askıya alma" +blockConfirm: "Onayı engelle" +unblockConfirm: "engellemeyi kaldır onayla" +selectChannel: "Kanal seç" +flagAsBot: "Bot olarak işaretle" +instances: "Sunucu" remove: "Sil" +pinnedNotes: "Sabitlenen" +userList: "Listeler" smtpUser: "Kullanıcı Adı" smtpPass: "Şifre" user: "Kullanıcı" searchByGoogle: "Arama" +_theme: + keys: + renote: "vazgeçme" _sfx: + note: "notlar" notification: "Bildirim" _widgets: profile: "Profil" notifications: "Bildirim" timeline: "Zaman çizelgesi" +_cw: + show: "Devamını yükle" +_visibility: + followers: "takipçi" _profile: username: "Kullanıcı Adı" +_exportOrImport: + followingList: "takipçi" + blockingList: "engelle" + userLists: "Listeler" +_notification: + _types: + follow: "takipçi" + renote: "vazgeçme" + quote: "alıntı" + _actions: + reply: "yanıt" + renote: "vazgeçme" _deck: _columns: notifications: "Bildirim" tl: "Zaman çizelgesi" + list: "Listeler" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 9c278ea751a..313c254c796 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1060,6 +1060,7 @@ cancelReactionConfirm: "要取消回应吗?" changeReactionConfirm: "要更改回应吗?" later: "一会再说" goToMisskey: "去往Misskey" +additionalEmojiDictionary: "表情符号追加字典" installed: "已安装" _initialAccountSetting: accountCreated: "账户创建完成了!" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index ef0baeef500..8017018507c 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1062,6 +1062,7 @@ later: "稍後再說" goToMisskey: "往Misskey" additionalEmojiDictionary: "表情符號的附加辭典" installed: "已安裝" +branding: "品牌宣傳" _initialAccountSetting: accountCreated: "帳戶已建立完成!" letsStartAccountSetup: "來進行帳戶的初始設定吧。" diff --git a/package.json b/package.json index eff0bf2dc2b..dd0c1d57e7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.13.1", + "version": "13.13.2", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 3d78015a532..1a66c197159 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -177,6 +177,17 @@ export class CacheService implements OnApplicationShutdown { @bindThis public dispose(): void { this.redisForSub.off('message', this.onMessage); + this.userByIdCache.dispose(); + this.localUserByNativeTokenCache.dispose(); + this.localUserByIdCache.dispose(); + this.uriPersonCache.dispose(); + this.userProfileCache.dispose(); + this.userMutingsCache.dispose(); + this.userBlockingCache.dispose(); + this.userBlockedCache.dispose(); + this.renoteMutingsCache.dispose(); + this.userFollowingsCache.dispose(); + this.userFollowingChannelsCache.dispose(); } @bindThis diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 3499df38b76..5f2ced77eb6 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { DataSource, In, IsNull } from 'typeorm'; import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; @@ -18,7 +18,7 @@ import type { Serialized } from '@/server/api/stream/types.js'; const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; @Injectable() -export class CustomEmojiService { +export class CustomEmojiService implements OnApplicationShutdown { private cache: MemoryKVCache; public localEmojisCache: RedisSingleCache>; @@ -349,4 +349,14 @@ export class CustomEmojiService { this.cache.set(`${emoji.name} ${emoji.host}`, emoji); } } + + @bindThis + public dispose(): void { + this.cache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index 8b9a87a380a..3603d59dcc2 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; import type { InstancesRepository } from '@/models/index.js'; import type { Instance } from '@/models/entities/Instance.js'; @@ -9,7 +9,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; @Injectable() -export class FederatedInstanceService { +export class FederatedInstanceService implements OnApplicationShutdown { public federatedInstanceCache: RedisKVCache; constructor( @@ -77,4 +77,14 @@ export class FederatedInstanceService { this.federatedInstanceCache.set(result.host, result); } + + @bindThis + public dispose(): void { + this.federatedInstanceCache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index a4c569bdec6..15a1d74878d 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import push from 'web-push'; import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; @@ -42,7 +42,7 @@ function truncateBody(type: T, body: Pus } @Injectable() -export class PushNotificationService { +export class PushNotificationService implements OnApplicationShutdown { private subscriptionsCache: RedisKVCache; constructor( @@ -115,4 +115,14 @@ export class PushNotificationService { }); } } + + @bindThis + public dispose(): void { + this.subscriptionsCache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 40ae1066627..79922d0a87e 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -435,6 +435,7 @@ export class RoleService implements OnApplicationShutdown { @bindThis public dispose(): void { this.redisForSub.off('message', this.onMessage); + this.roleAssignmentByUserIdCache.dispose(); } @bindThis diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 72c35c529c2..d768f086503 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; import type { User } from '@/models/entities/User.js'; import type { UserKeypairsRepository } from '@/models/index.js'; @@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @Injectable() -export class UserKeypairService { +export class UserKeypairService implements OnApplicationShutdown { private cache: RedisKVCache; constructor( @@ -31,4 +31,14 @@ export class UserKeypairService { public async getUserKeypair(userId: User['id']): Promise { return await this.cache.fetch(userId); } + + @bindThis + public dispose(): void { + this.cache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 2b404ebecae..2d9e7a14ee2 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import escapeRegexp from 'escape-regexp'; import { DI } from '@/di-symbols.js'; import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; @@ -30,7 +30,7 @@ export type UriParseResult = { }; @Injectable() -export class ApDbResolverService { +export class ApDbResolverService implements OnApplicationShutdown { private publicKeyCache: MemoryKVCache; private publicKeyByUserIdCache: MemoryKVCache; @@ -162,4 +162,15 @@ export class ApDbResolverService { key, }; } + + @bindThis + public dispose(): void { + this.publicKeyCache.dispose(); + this.publicKeyByUserIdCache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 5610929648b..f130a7db8b8 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -83,6 +83,16 @@ export class RedisKVCache { // TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする } + + @bindThis + public gc() { + this.memoryCache.gc(); + } + + @bindThis + public dispose() { + this.memoryCache.dispose(); + } } export class RedisSingleCache { @@ -174,10 +184,15 @@ export class RedisSingleCache { export class MemoryKVCache { public cache: Map; private lifetime: number; + private gcIntervalHandle: NodeJS.Timer; constructor(lifetime: MemoryKVCache['lifetime']) { this.cache = new Map(); this.lifetime = lifetime; + + this.gcIntervalHandle = setInterval(() => { + this.gc(); + }, 1000 * 60 * 3); } @bindThis @@ -200,7 +215,7 @@ export class MemoryKVCache { } @bindThis - public delete(key: string) { + public delete(key: string): void { this.cache.delete(key); } @@ -255,6 +270,21 @@ export class MemoryKVCache { } return value; } + + @bindThis + public gc(): void { + const now = Date.now(); + for (const [key, { date }] of this.cache.entries()) { + if ((now - date) > this.lifetime) { + this.cache.delete(key); + } + } + } + + @bindThis + public dispose(): void { + clearInterval(this.gcIntervalHandle); + } } export class MemorySingleCache { diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index e7a31477e23..89283a4b521 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; import type { LocalUser } from '@/models/entities/User.js'; @@ -18,7 +18,7 @@ export class AuthenticationError extends Error { } @Injectable() -export class AuthenticateService { +export class AuthenticateService implements OnApplicationShutdown { private appCache: MemoryKVCache; constructor( @@ -91,4 +91,14 @@ export class AuthenticateService { } } } + + @bindThis + public dispose(): void { + this.appCache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts index d61c6b8dc6a..5ad29839c2c 100644 --- a/packages/backend/src/server/api/endpoints/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/roles/list.ts @@ -30,6 +30,7 @@ export default class extends Endpoint { super(meta, paramDef, async (ps, me) => { const roles = await this.rolesRepository.findBy({ isPublic: true, + isExplorable: true, }); return await this.roleEntityService.packMany(roles, me); }); diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 607dc242069..b2cb8b42a85 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -49,6 +49,7 @@ export default class extends Endpoint { const role = await this.rolesRepository.findOneBy({ id: ps.roleId, isPublic: true, + isExplorable: true, }); if (role == null) { diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue index 24b08351351..503e00387c3 100644 --- a/packages/frontend/src/components/global/MkError.vue +++ b/packages/frontend/src/components/global/MkError.vue @@ -1,7 +1,7 @@