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 ## 13.13.1
### Client ### Client
- フォロー/フォロワーを非公開としている場合、表示は「0」ではなく鍵アイコンを表示するように
- Fix: タブがアクティブな間はstreamが切断されないように - Fix: タブがアクティブな間はstreamが切断されないように
### Server ### Server
@ -118,7 +117,6 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
- Node.js 18.16.0以上が必要になりました - Node.js 18.16.0以上が必要になりました
### General ### 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を全文検索に使用できるようになりました - 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)) - 리모트에 존재하는 커스텀 이모지도 자신의 서버 내에 같은 이름의 이모지가 있으면 리액션 할 수 있도록 ([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) - 이벤트 기능 (misskey-dev/misskey#10628)
- プレイにAPI Tokenを要求できる関数を追加 (misskey-dev/misskey#10949)
### Client ### Client
- (Friendly) 일부 페이지를 제외하고 플로팅 버튼을 표시하지 않음 - (Friendly) 일부 페이지를 제외하고 플로팅 버튼을 표시하지 않음

View file

@ -1111,6 +1111,9 @@ goToMisskey: "To CherryPick"
additionalEmojiDictionary: "Additional emoji dictionaries" additionalEmojiDictionary: "Additional emoji dictionaries"
installed: "Installed" installed: "Installed"
branding: "Branding" 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" translateProfile: "Translate profile"
_group: _group:
leader: "Group owner" leader: "Group owner"

3
locales/index.d.ts vendored
View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { StreamMessages } from '@/server/api/stream/types.js';
import type { FlashToken } from '@/misc/flash-token';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
@ -16,6 +17,7 @@ export class CacheService implements OnApplicationShutdown {
public localUserByIdCache: MemoryKVCache<LocalUser>; public localUserByIdCache: MemoryKVCache<LocalUser>;
public uriPersonCache: MemoryKVCache<User | null>; public uriPersonCache: MemoryKVCache<User | null>;
public userProfileCache: RedisKVCache<UserProfile>; public userProfileCache: RedisKVCache<UserProfile>;
public flashAccessTokensCache: RedisKVCache<FlashToken | null>;
public userMutingsCache: RedisKVCache<Set<string>>; public userMutingsCache: RedisKVCache<Set<string>>;
public userBlockingCache: RedisKVCache<Set<string>>; public userBlockingCache: RedisKVCache<Set<string>>;
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
@ -116,6 +118,13 @@ export class CacheService implements OnApplicationShutdown {
fromRedisConverter: (value) => new Set(JSON.parse(value)), 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); 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 { FastifyRequest, FastifyReply } from 'fastify';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
import type { IEndpointMeta, IEndpoint } from './endpoints.js'; import type { IEndpointMeta, IEndpoint } from './endpoints.js';
import type { FlashToken } from '@/misc/flash-token.js';
const pump = promisify(pipeline); const pump = promisify(pipeline);
@ -68,8 +69,8 @@ export class ApiCallService implements OnApplicationShutdown {
reply.code(400); reply.code(400);
return; return;
} }
this.authenticateService.authenticate(token).then(([user, app]) => { this.authenticateService.authenticate(token).then(([user, app, flashToken]) => {
this.call(endpoint, user, app, body, null, request).then((res) => { this.call(endpoint, user, app, flashToken, body, null, request).then((res) => {
if (request.method === 'GET' && endpoint.meta.cacheSec && !body?.['i'] && !user) { if (request.method === 'GET' && endpoint.meta.cacheSec && !body?.['i'] && !user) {
reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`); reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
} }
@ -122,8 +123,8 @@ export class ApiCallService implements OnApplicationShutdown {
reply.code(400); reply.code(400);
return; return;
} }
this.authenticateService.authenticate(token).then(([user, app]) => { this.authenticateService.authenticate(token).then(([user, app, flashToken]) => {
this.call(endpoint, user, app, fields, { this.call(endpoint, user, app, flashToken, fields, {
name: multipartData.filename, name: multipartData.filename,
path: path, path: path,
}, request).then((res) => { }, request).then((res) => {
@ -199,6 +200,7 @@ export class ApiCallService implements OnApplicationShutdown {
ep: IEndpoint & { exec: any }, ep: IEndpoint & { exec: any },
user: LocalUser | null | undefined, user: LocalUser | null | undefined,
token: AccessToken | null | undefined, token: AccessToken | null | undefined,
flashToken: FlashToken | null | undefined,
data: any, data: any,
file: { file: {
name: string; name: string;
@ -206,7 +208,7 @@ export class ApiCallService implements OnApplicationShutdown {
} | null, } | null,
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>, 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) { if (ep.meta.secure && !isSecure) {
throw new ApiError(accessDenied); 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 // Cast non JSON input
if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) { if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) {
for (const k of Object.keys(ep.params.properties)) { for (const k of Object.keys(ep.params.properties)) {
@ -331,7 +341,7 @@ export class ApiCallService implements OnApplicationShutdown {
} }
// API invoking // 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) { if (err instanceof ApiError || err instanceof AuthenticationError) {
throw err; throw err;
} else { } else {

View file

@ -8,6 +8,7 @@ import type { App } from '@/models/entities/App.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import isNativeToken from '@/misc/is-native-token.js'; import isNativeToken from '@/misc/is-native-token.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { FlashToken } from '@/misc/flash-token';
export class AuthenticationError extends Error { export class AuthenticationError extends Error {
constructor(message: string) { constructor(message: string) {
@ -36,9 +37,9 @@ export class AuthenticateService implements OnApplicationShutdown {
} }
@bindThis @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) { if (token == null) {
return [null, null]; return [null, null, null];
} }
if (isNativeToken(token)) { if (isNativeToken(token)) {
@ -49,7 +50,7 @@ export class AuthenticateService implements OnApplicationShutdown {
throw new AuthenticationError('user not found'); throw new AuthenticationError('user not found');
} }
return [user, null]; return [user, null, null];
} else { } else {
const accessToken = await this.accessTokensRepository.findOne({ const accessToken = await this.accessTokensRepository.findOne({
where: [{ where: [{
@ -60,7 +61,12 @@ export class AuthenticateService implements OnApplicationShutdown {
}); });
if (accessToken == null) { 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, { this.accessTokensRepository.update(accessToken.id, {
@ -79,9 +85,9 @@ export class AuthenticateService implements OnApplicationShutdown {
return [user, { return [user, {
id: accessToken.id, id: accessToken.id,
permission: app.permission, permission: app.permission,
} as AccessToken]; } as AccessToken, null];
} else { } 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_create from './endpoints/flash/create.js';
import * as ep___flash_delete from './endpoints/flash/delete.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_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_like from './endpoints/flash/like.js';
import * as ep___flash_show from './endpoints/flash/show.js'; import * as ep___flash_show from './endpoints/flash/show.js';
import * as ep___flash_unlike from './endpoints/flash/unlike.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_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_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_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_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_show: Provider = { provide: 'ep:flash/show', useClass: ep___flash_show.default };
const $flash_unlike: Provider = { provide: 'ep:flash/unlike', useClass: ep___flash_unlike.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_create,
$flash_delete, $flash_delete,
$flash_featured, $flash_featured,
$flash_genToken,
$flash_like, $flash_like,
$flash_show, $flash_show,
$flash_unlike, $flash_unlike,
@ -1380,6 +1383,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$flash_create, $flash_create,
$flash_delete, $flash_delete,
$flash_featured, $flash_featured,
$flash_genToken,
$flash_like, $flash_like,
$flash_show, $flash_show,
$flash_unlike, $flash_unlike,

View file

@ -3,6 +3,7 @@ import Ajv from 'ajv';
import type { Schema, SchemaType } from '@/misc/json-schema.js'; import type { Schema, SchemaType } from '@/misc/json-schema.js';
import type { LocalUser } from '@/models/entities/User.js'; import type { LocalUser } from '@/models/entities/User.js';
import type { AccessToken } from '@/models/entities/AccessToken.js'; import type { AccessToken } from '@/models/entities/AccessToken.js';
import type { FlashToken } from '@/misc/flash-token.js';
import { ApiError } from './error.js'; import { ApiError } from './error.js';
import type { IEndpointMeta } from './endpoints.js'; import type { IEndpointMeta } from './endpoints.js';
@ -21,16 +22,16 @@ type File = {
// TODO: paramsの型をT['params']のスキーマ定義から推論する // TODO: paramsの型をT['params']のスキーマ定義から推論する
type Executor<T extends IEndpointMeta, Ps extends Schema> = 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']>>>; Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> { 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>) { constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
const validate = ajv.compile(paramDef); 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; let cleanup: undefined | (() => void) = undefined;
if (meta.requireFile) { if (meta.requireFile) {
@ -61,7 +62,7 @@ export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
return Promise.reject(err); 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_create from './endpoints/flash/create.js';
import * as ep___flash_delete from './endpoints/flash/delete.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_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_like from './endpoints/flash/like.js';
import * as ep___flash_show from './endpoints/flash/show.js'; import * as ep___flash_show from './endpoints/flash/show.js';
import * as ep___flash_unlike from './endpoints/flash/unlike.js'; import * as ep___flash_unlike from './endpoints/flash/unlike.js';
@ -653,6 +654,7 @@ const eps = [
['flash/create', ep___flash_create], ['flash/create', ep___flash_create],
['flash/delete', ep___flash_delete], ['flash/delete', ep___flash_delete],
['flash/featured', ep___flash_featured], ['flash/featured', ep___flash_featured],
['flash/gen-token', ep___flash_genToken],
['flash/like', ep___flash_like], ['flash/like', ep___flash_like],
['flash/show', ep___flash_show], ['flash/show', ep___flash_show],
['flash/unlike', ep___flash_unlike], ['flash/unlike', ep___flash_unlike],

View file

@ -40,8 +40,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private appEntityService: AppEntityService, private appEntityService: AppEntityService,
) { ) {
super(meta, paramDef, async (ps, user, token) => { super(meta, paramDef, async (ps, user, token, flashToken) => {
const isSecure = user != null && token == null; const isSecure = user != null && token == null && flashToken == null;
// Lookup app // Lookup app
const ap = await this.appsRepository.findOneBy({ id: ps.appId }); 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 metaService: MetaService,
private driveService: DriveService, 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 // Get 'name' parameter
let name = ps.name ?? file!.name ?? null; let name = ps.name ?? file!.name ?? null;
if (name != null) { if (name != null) {

View file

@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private driveService: DriveService, private driveService: DriveService,
private globalEventService: GlobalEventService, 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.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.driveFileEntityService.pack(file, { self: true }).then(packedFile => {
this.globalEventService.publishMainStream(user.id, 'urlUploadFinished', { 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, private userEntityService: UserEntityService,
) { ) {
super(meta, paramDef, async (ps, user, token) => { super(meta, paramDef, async (ps, user, token, flashToken) => {
const isSecure = token == null; const isSecure = token == null && flashToken == null;
const now = new Date(); const now = new Date();
const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`; 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 roleService: RoleService,
private cacheService: CacheService, 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 user = await this.usersRepository.findOneByOrFail({ id: _user.id });
const isSecure = token == null; const isSecure = token == null && flashToken == null;
const updates = {} as Partial<User>; const updates = {} as Partial<User>;
const profileUpdates = {} as Partial<UserProfile>; const profileUpdates = {} as Partial<UserProfile>;

View file

@ -87,7 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private perUserPvChart: PerUserPvChart, private perUserPvChart: PerUserPvChart,
private apiLoggerService: ApiLoggerService, 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; let user;
const isModerator = await this.roleService.isModerator(me); 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/remove-key': { req: TODO; res: TODO; };
'i/2fa/unregister': { req: TODO; res: TODO; }; 'i/2fa/unregister': { req: TODO; res: TODO; };
// flash
'flash/gen-token': { req: TODO; res: TODO; };
// messaging // messaging
'messaging/history': { req: { limit?: number; group?: boolean; }; res: MessagingMessage[]; }; 'messaging/history': { req: { limit?: number; group?: boolean; }; res: MessagingMessage[]; };
'messaging/messages': { req: { userId?: User['id']; groupId?: UserGroup['id']; limit?: number; sinceId?: MessagingMessage['id']; untilId?: MessagingMessage['id']; markAsRead?: boolean; }; res: MessagingMessage[]; }; 'messaging/messages': { 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}` | `ui:folder:${string}` |
`themes:${string}` | `themes:${string}` |
`aiscript:${string}` | `aiscript:${string}` |
`aiscriptSecure:${string}` |
'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~) 'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~)
'emojis' // 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 { utils, values } from '@syuilo/aiscript';
import { defineAsyncComponent } from 'vue';
import { permissions as MkPermissions } from 'misskey-js';
import * as os from '@/os'; import * as os from '@/os';
import { $i } from '@/account'; import { $i } from '@/account';
import { miLocalStorage } from '@/local-storage'; import { miLocalStorage } from '@/local-storage';
@ -6,6 +8,10 @@ import { customEmojis } from '@/custom-emojis';
export function createAiScriptEnv(opts) { export function createAiScriptEnv(opts) {
let apiRequests = 0; let apiRequests = 0;
const table = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const randomString = Array.from(crypto.getRandomValues(new Uint32Array(32)))
.map(v => table[v % table.length])
.join('');
return { return {
USER_ID: $i ? values.STR($i.id) : values.NULL, USER_ID: $i ? values.STR($i.id) : values.NULL,
USER_NAME: $i ? values.STR($i.name) : values.NULL, USER_NAME: $i ? values.STR($i.name) : values.NULL,
@ -35,7 +41,7 @@ export function createAiScriptEnv(opts) {
} }
apiRequests++; apiRequests++;
if (apiRequests > 16) return values.NULL; 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); return utils.jsToVal(res);
}), }),
'Mk:save': values.FN_NATIVE(([key, value]) => { 'Mk:save': values.FN_NATIVE(([key, value]) => {
@ -47,5 +53,32 @@ export function createAiScriptEnv(opts) {
utils.assertString(key); utils.assertString(key);
return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`))); 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');
});
}),
}; };
} }