Skip to content

Commit

Permalink
Merge pull request #10949 from misskey-dev
Browse files Browse the repository at this point in the history
  • Loading branch information
noridev committed Jun 18, 2023
2 parents 81fa487 + f495296 commit 54545fb
Show file tree
Hide file tree
Showing 24 changed files with 227 additions and 28 deletions.
2 changes: 0 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
## 13.13.1

### Client
- フォロー/フォロワーを非公開としている場合、表示は「0」ではなく鍵アイコンを表示するように
- Fix: タブがアクティブな間はstreamが切断されないように

### Server
Expand Down Expand Up @@ -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を全文検索に使用できるようになりました
* 「フォロワーのみ」の投稿は検索結果に表示されません。
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG_CHERRYPICK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) 일부 페이지를 제외하고 플로팅 버튼을 표시하지 않음
Expand Down
3 changes: 3 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,9 @@ export interface Locale {
"goToMisskey": string;
"additionalEmojiDictionary": string;
"installed": string;
"additionalPermissionsForFlash": string;
"thisFlashRequiresTheFollowingPermissions": string;
"doYouWantToAllowThisPlayToAccessYourAccount": string;
"branding": string;
"translateProfile": string;
"_group": {
Expand Down
3 changes: 3 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,9 @@ goToMisskey: "CherryPickへ"
additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み"
branding: "ブランディング"
additionalPermissionsForFlash: "Playへの追加許可"
thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求しています"
doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか?"
translateProfile: "プロフィールを翻訳する"

_group:
Expand Down
3 changes: 3 additions & 0 deletions locales/ko-KR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,9 @@ later: "나중에"
goToMisskey: "CherryPick으로"
additionalEmojiDictionary: "이모지 추가 사전"
installed: "설치됨"
additionalPermissionsForFlash: "Play에 대한 추가 권한"
thisFlashRequiresTheFollowingPermissions: "이 Play는 다음 권한을 요구해요"
doYouWantToAllowThisPlayToAccessYourAccount: "이 Play가 계정에 접근하도록 허용할까요?"
translateProfile: "프로필 번역하기"
_group:
leader: "그룹 주인"
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/core/CacheService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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キャッシュ
Expand Down Expand Up @@ -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);
}

Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/misc/flash-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { LocalUser } from '@/models/entities/User.js';

export type FlashToken = {
permissions: string[];
user: LocalUser
};
22 changes: 16 additions & 6 deletions packages/backend/src/server/api/ApiCallService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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}`);
}
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -199,14 +200,15 @@ 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;
path: string;
} | 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);
Expand Down Expand Up @@ -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)) {
Expand All @@ -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 {
Expand Down
18 changes: 12 additions & 6 deletions packages/backend/src/server/api/AuthenticateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)) {
Expand All @@ -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: [{
Expand All @@ -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, {
Expand All @@ -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];
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/server/api/EndpointsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions packages/backend/src/server/api/endpoint-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) {
Expand Down Expand Up @@ -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);
};
}
}
2 changes: 2 additions & 0 deletions packages/backend/src/server/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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],
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/server/api/endpoints/app/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
56 changes: 56 additions & 0 deletions packages/backend/src/server/api/endpoints/flash/gen-token.ts
Original file line number Diff line number Diff line change
@@ -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,
};
});
}
}
Loading

0 comments on commit 54545fb

Please sign in to comment.