Merge pull request #10949 from misskey-dev
This commit is contained in:
commit
54545fb501
|
@ -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を全文検索に使用できるようになりました
|
||||
* 「フォロワーのみ」の投稿は検索結果に表示されません。
|
||||
|
|
|
@ -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) 일부 페이지를 제외하고 플로팅 버튼을 표시하지 않음
|
||||
|
|
|
@ -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
3
locales/index.d.ts
vendored
|
@ -1113,6 +1113,9 @@ export interface Locale {
|
|||
"goToMisskey": string;
|
||||
"additionalEmojiDictionary": string;
|
||||
"installed": string;
|
||||
"additionalPermissionsForFlash": string;
|
||||
"thisFlashRequiresTheFollowingPermissions": string;
|
||||
"doYouWantToAllowThisPlayToAccessYourAccount": string;
|
||||
"branding": string;
|
||||
"translateProfile": string;
|
||||
"_group": {
|
||||
|
|
|
@ -1111,6 +1111,9 @@ goToMisskey: "CherryPickへ"
|
|||
additionalEmojiDictionary: "絵文字の追加辞書"
|
||||
installed: "インストール済み"
|
||||
branding: "ブランディング"
|
||||
additionalPermissionsForFlash: "Playへの追加許可"
|
||||
thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求しています"
|
||||
doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか?"
|
||||
translateProfile: "プロフィールを翻訳する"
|
||||
|
||||
_group:
|
||||
|
|
|
@ -1111,6 +1111,9 @@ later: "나중에"
|
|||
goToMisskey: "CherryPick으로"
|
||||
additionalEmojiDictionary: "이모지 추가 사전"
|
||||
installed: "설치됨"
|
||||
additionalPermissionsForFlash: "Play에 대한 추가 권한"
|
||||
thisFlashRequiresTheFollowingPermissions: "이 Play는 다음 권한을 요구해요"
|
||||
doYouWantToAllowThisPlayToAccessYourAccount: "이 Play가 계정에 접근하도록 허용할까요?"
|
||||
translateProfile: "프로필 번역하기"
|
||||
_group:
|
||||
leader: "그룹 주인"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
6
packages/backend/src/misc/flash-token.ts
Normal file
6
packages/backend/src/misc/flash-token.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
|
||||
export type FlashToken = {
|
||||
permissions: string[];
|
||||
user: LocalUser
|
||||
};
|
|
@ -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 {
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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', {
|
||||
|
|
56
packages/backend/src/server/api/endpoints/flash/gen-token.ts
Normal file
56
packages/backend/src/server/api/endpoints/flash/gen-token.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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()}`;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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[]; };
|
||||
|
|
|
@ -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>
|
|
@ -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~);
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue