Merge pull request #10949 from misskey-dev

This commit is contained in:
NoriDev 2023-06-18 19:37:54 +09:00
commit 54545fb501
24 changed files with 227 additions and 28 deletions

View file

@ -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を全文検索に使用できるようになりました
* 「フォロワーのみ」の投稿は検索結果に表示されません。

View file

@ -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) 일부 페이지를 제외하고 플로팅 버튼을 표시하지 않음

View file

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

3
locales/index.d.ts vendored
View file

@ -1113,6 +1113,9 @@ export interface Locale {
"goToMisskey": string;
"additionalEmojiDictionary": string;
"installed": string;
"additionalPermissionsForFlash": string;
"thisFlashRequiresTheFollowingPermissions": string;
"doYouWantToAllowThisPlayToAccessYourAccount": string;
"branding": string;
"translateProfile": string;
"_group": {

View file

@ -1111,6 +1111,9 @@ goToMisskey: "CherryPickへ"
additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み"
branding: "ブランディング"
additionalPermissionsForFlash: "Playへの追加許可"
thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求しています"
doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか"
translateProfile: "プロフィールを翻訳する"
_group:

View file

@ -1111,6 +1111,9 @@ later: "나중에"
goToMisskey: "CherryPick으로"
additionalEmojiDictionary: "이모지 추가 사전"
installed: "설치됨"
additionalPermissionsForFlash: "Play에 대한 추가 권한"
thisFlashRequiresTheFollowingPermissions: "이 Play는 다음 권한을 요구해요"
doYouWantToAllowThisPlayToAccessYourAccount: "이 Play가 계정에 접근하도록 허용할까요?"
translateProfile: "프로필 번역하기"
_group:
leader: "그룹 주인"

View file

@ -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<LocalUser>;
public uriPersonCache: MemoryKVCache<User | null>;
public userProfileCache: RedisKVCache<UserProfile>;
public flashAccessTokensCache: RedisKVCache<FlashToken | null>;
public userMutingsCache: RedisKVCache<Set<string>>;
public userBlockingCache: RedisKVCache<Set<string>>;
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
@ -116,6 +118,13 @@ export class CacheService implements OnApplicationShutdown {
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.flashAccessTokensCache = new RedisKVCache<FlashToken | null>(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);
}

View file

@ -0,0 +1,6 @@
import type { LocalUser } from '@/models/entities/User.js';
export type FlashToken = {
permissions: string[];
user: LocalUser
};

View file

@ -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<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
) {
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 {

View file

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

View file

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

View file

@ -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<T extends IEndpointMeta, Ps extends Schema> =
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, flashToken: FlashToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
public exec: (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
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<string, string> | null) => Promise<any>;
constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
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<string, string> | 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<string, string> | null) => {
let cleanup: undefined | (() => void) = undefined;
if (meta.requireFile) {
@ -61,7 +62,7 @@ export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
return Promise.reject(err);
}
return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers);
return cb(params as SchemaType<Ps>, user, token, flashToken, file, cleanup, ip, headers);
};
}
}

View file

@ -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],

View file

@ -40,8 +40,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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 });

View file

@ -78,7 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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) {

View file

@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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', {

View file

@ -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<typeof meta, typeof paramDef> {
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,
};
});
}
}

View file

@ -44,8 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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()}`;

View file

@ -194,9 +194,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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<User>;
const profileUpdates = {} as Partial<UserProfile>;

View file

@ -87,7 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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);

View file

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

View file

@ -0,0 +1,58 @@
<template>
<MkModalWindow
ref="dialog"
:width="400"
:height="450"
@close="dialog!.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.additionalPermissionsForFlash }}</template>
<MkSpacer :marginMin="20" :marginMax="28">
<p>{{ i18n.ts.thisFlashRequiresTheFollowingPermissions }}</p>
<ul>
<li v-for="permission in props.permissions" :key="permission">
{{ i18n.t(`_permissions.${permission}`) }}
</li>
</ul>
<p>{{ i18n.ts.doYouWantToAllowThisPlayToAccessYourAccount }}</p>
<div>
<MkButton inline :class="$style.cancel" @click="cancel">{{ i18n.ts.cancel }}</MkButton>
<MkButton primary inline @click="accept">{{ i18n.ts.accept }}</MkButton>
</div>
</MkSpacer>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MkModalWindow from './MkModalWindow.vue';
import MkSpacer from './global/MkSpacer.vue';
import MkButton from './MkButton.vue';
import { i18n } from '@/i18n';
const props = defineProps<{
permissions: string[];
}>();
const emit = defineEmits<{
(ev: 'closed'): void,
(ev: 'accept'): void,
(ev: 'cancel'): void,
}>();
const dialog = ref<InstanceType<typeof MkModalWindow>>();
const cancel = () => {
emit('cancel');
dialog.value?.close();
};
const accept = () => {
emit('accept');
dialog.value?.close();
};
</script>
<style lang="scss" module>
.cancel {
margin-right: 5px;
}
</style>

View file

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

View file

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