diff --git a/CHANGELOG.md b/CHANGELOG.md index 832f927b6c..681105fb7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,6 @@ ## 13.13.1 ### Client -- フォロー/フォロワーを非公開としている場合、表示は「0」ではなく鍵アイコンを表示するように - Fix: タブがアクティブな間はstreamが切断されないように ### Server @@ -118,7 +117,6 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー - Node.js 18.16.0以上が必要になりました ### General -- Add support for user created events. Includes basic federation of ActivityPub Event objects. [PR 10628](https://github.com/misskey-dev/misskey/pull/10628) @ssmucny - アカウントの引っ越し(フォロワー引き継ぎ)に対応 - Meilisearchを全文検索に使用できるようになりました * 「フォロワーのみ」の投稿は検索結果に表示されません。 diff --git a/CHANGELOG_CHERRYPICK.md b/CHANGELOG_CHERRYPICK.md index 604cf1a36b..81a0dc4fa3 100644 --- a/CHANGELOG_CHERRYPICK.md +++ b/CHANGELOG_CHERRYPICK.md @@ -45,6 +45,7 @@ - 리모트에 존재하는 커스텀 이모지도 자신의 서버 내에 같은 이름의 이모지가 있으면 리액션 할 수 있도록 ([shrimpia/misskey@e91295f](https://github.com/shrimpia/misskey/commit/e91295ff9c6f8ac90f61c8de7a891a6836e48e95), [shrimpia/misskey@010378f](https://github.com/shrimpia/misskey/commit/010378fae659ad3015bfade4346209e01bb2a902), [shrimpia/misskey@acf2a30](https://github.com/shrimpia/misskey/commit/acf2a30e8a8c57525dfbab499dbb0b6c7d8e43c2)) - 「이미 본 리노트를 간략화하기」 옵션의 기본값을 꺼짐으로 설정 - 이벤트 기능 (misskey-dev/misskey#10628) +- プレイにAPI Tokenを要求できる関数を追加 (misskey-dev/misskey#10949) ### Client - (Friendly) 일부 페이지를 제외하고 플로팅 버튼을 표시하지 않음 diff --git a/locales/en-US.yml b/locales/en-US.yml index 1d6177334d..6523ca9821 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1111,6 +1111,9 @@ goToMisskey: "To CherryPick" additionalEmojiDictionary: "Additional emoji dictionaries" installed: "Installed" branding: "Branding" +additionalPermissionsForFlash: "Allow to add permission to Play" +thisFlashRequiresTheFollowingPermissions: "This Play requires the following permissions" +doYouWantToAllowThisPlayToAccessYourAccount: "Do you want to allow this Play to access your account?" translateProfile: "Translate profile" _group: leader: "Group owner" diff --git a/locales/index.d.ts b/locales/index.d.ts index f232d88b7a..77704d97eb 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1113,6 +1113,9 @@ export interface Locale { "goToMisskey": string; "additionalEmojiDictionary": string; "installed": string; + "additionalPermissionsForFlash": string; + "thisFlashRequiresTheFollowingPermissions": string; + "doYouWantToAllowThisPlayToAccessYourAccount": string; "branding": string; "translateProfile": string; "_group": { diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d93585f86a..9c47fa95df 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1111,6 +1111,9 @@ goToMisskey: "CherryPickへ" additionalEmojiDictionary: "絵文字の追加辞書" installed: "インストール済み" branding: "ブランディング" +additionalPermissionsForFlash: "Playへの追加許可" +thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求しています" +doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか?" translateProfile: "プロフィールを翻訳する" _group: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 97c0acebf6..d33d4831a0 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1111,6 +1111,9 @@ later: "나중에" goToMisskey: "CherryPick으로" additionalEmojiDictionary: "이모지 추가 사전" installed: "설치됨" +additionalPermissionsForFlash: "Play에 대한 추가 권한" +thisFlashRequiresTheFollowingPermissions: "이 Play는 다음 권한을 요구해요" +doYouWantToAllowThisPlayToAccessYourAccount: "이 Play가 계정에 접근하도록 허용할까요?" translateProfile: "프로필 번역하기" _group: leader: "그룹 주인" diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 2b7f9a48da..1a66c19715 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 0000000000..2622f955df --- /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 dad1a4132a..a9d750f351 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)) { @@ -331,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/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index 4ad0197d87..89283a4b52 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 implements OnApplicationShutdown { } @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 implements OnApplicationShutdown { 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 implements OnApplicationShutdown { }); 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 implements OnApplicationShutdown { 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 4ff4406e99..6e10aa0142 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -293,6 +293,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'; @@ -655,6 +656,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 }; @@ -1021,6 +1023,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $flash_create, $flash_delete, $flash_featured, + $flash_genToken, $flash_like, $flash_show, $flash_unlike, @@ -1380,6 +1383,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/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts index 1555a3ca46..5aae602c00 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.ts b/packages/backend/src/server/api/endpoints.ts index f36a37a6cd..de713a94f6 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -293,6 +293,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'; @@ -653,6 +654,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/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts index eaafa8dc1b..52e9c02aba 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 a1c1f9325e..e128e80944 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 c835587c4a..4edc4cdc9f 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/flash/gen-token.ts b/packages/backend/src/server/api/endpoints/flash/gen-token.ts new file mode 100644 index 0000000000..bcbce360f1 --- /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.ts b/packages/backend/src/server/api/endpoints/i.ts index a3e3e02a12..f336caeb77 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 8a82994d3b..179f2244f8 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 ba432c273b..ed42c66de8 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); diff --git a/packages/cherrypick-js/src/api.types.ts b/packages/cherrypick-js/src/api.types.ts index 7d12716bd7..4c7b7c9feb 100644 --- a/packages/cherrypick-js/src/api.types.ts +++ b/packages/cherrypick-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[]; }; diff --git a/packages/frontend/src/components/MkFlashRequestTokenDialog.vue b/packages/frontend/src/components/MkFlashRequestTokenDialog.vue new file mode 100644 index 0000000000..9f65f21168 --- /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 b1b9828b28..4326005a7d 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 b6b7445b67..64a5f41b3a 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'); + }); + }), }; }