From 7bb8c71543a7a41aeab6dad4edbb4088f7ae5126 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 25 Jun 2023 01:34:18 +0200 Subject: chore(backend, misskey-js): add type for signup (#11043) * chore(backend, misskey-js): add type for signup * rerun --- packages/backend/test/utils.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) (limited to 'packages/backend/test/utils.ts') diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 22f7d81e4e..eeb1b19daa 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -83,7 +83,7 @@ const relativeFetch = async (path: string, init?: RequestInit | undefined) => { return await fetch(new URL(path, `http://127.0.0.1:${port}/`).toString(), init); }; -export const signup = async (params?: any): Promise => { +export const signup = async (params?: Partial): Promise> => { const q = Object.assign({ username: 'test', password: 'test', @@ -213,8 +213,8 @@ export const role = async (user: any, role: any = {}, policies: any = {}): Promi isPublic: false, name: 'New Role', target: 'manual', - policies: { - ...Object.entries(DEFAULT_POLICIES).map(([k, v]) => [k, { + policies: { + ...Object.entries(DEFAULT_POLICIES).map(([k, v]) => [k, { priority: 0, useDefault: true, value: v, @@ -351,11 +351,11 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond }); }; -export type SimpleGetResponse = { - status: number, - body: any | JSDOM | null, - type: string | null, - location: string | null +export type SimpleGetResponse = { + status: number, + body: any | JSDOM | null, + type: string | null, + location: string | null }; export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined): Promise => { const res = await relativeFetch(path, { @@ -374,9 +374,9 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde 'text/html; charset=utf-8', ]; - const body = - jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : - htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : + const body = + jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : + htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : null; return { -- cgit v1.2.3-freya From d48172e9d13cfc907b746760e7ac5f9e902ddcf1 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Tue, 27 Jun 2023 01:07:20 +0200 Subject: refactor(backend/test): add `interface UserToken` (#11050) --- packages/backend/test/utils.ts | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) (limited to 'packages/backend/test/utils.ts') diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index eeb1b19daa..8583f024cb 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -13,14 +13,16 @@ import type * as misskey from 'misskey-js'; export { server as startServer } from '@/boot/common.js'; +interface UserToken { token: string } + const config = loadConfig(); export const port = config.port; -export const cookie = (me: any): string => { +export const cookie = (me: UserToken): string => { return `token=${me.token};`; }; -export const api = async (endpoint: string, params: any, me?: any) => { +export const api = async (endpoint: string, params: any, me?: UserToken) => { const normalized = endpoint.replace(/^\//, ''); return await request(`api/${normalized}`, params, me); }; @@ -28,7 +30,7 @@ export const api = async (endpoint: string, params: any, me?: any) => { export type ApiRequest = { endpoint: string, parameters: object, - user: object | undefined, + user: UserToken | undefined, }; export const successfulApiCall = async (request: ApiRequest, assertion: { @@ -55,7 +57,7 @@ export const failedApiCall = async (request: ApiRequest, assertion: { return res.body; }; -const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => { +const request = async (path: string, params: any, me?: UserToken): Promise<{ body: any, status: number }> => { const auth = me ? { i: me.token, } : {}; @@ -94,7 +96,7 @@ export const signup = async (params?: Partial => { +export const post = async (user: UserToken, params?: misskey.Endpoints['notes/create']['req']): Promise => { const q = params; const res = await api('notes/create', q, user); @@ -117,21 +119,21 @@ export const hiddenNote = (note: any): any => { return temp; }; -export const react = async (user: any, note: any, reaction: string): Promise => { +export const react = async (user: UserToken, note: any, reaction: string): Promise => { await api('notes/reactions/create', { noteId: note.id, reaction: reaction, }, user); }; -export const userList = async (user: any, userList: any = {}): Promise => { +export const userList = async (user: UserToken, userList: any = {}): Promise => { const res = await api('users/lists/create', { name: 'test', }, user); return res.body; }; -export const page = async (user: any, page: any = {}): Promise => { +export const page = async (user: UserToken, page: any = {}): Promise => { const res = await api('pages/create', { alignCenter: false, content: [ @@ -154,7 +156,7 @@ export const page = async (user: any, page: any = {}): Promise => { return res.body; }; -export const play = async (user: any, play: any = {}): Promise => { +export const play = async (user: UserToken, play: any = {}): Promise => { const res = await api('flash/create', { permissions: [], script: 'test', @@ -165,7 +167,7 @@ export const play = async (user: any, play: any = {}): Promise => { return res.body; }; -export const clip = async (user: any, clip: any = {}): Promise => { +export const clip = async (user: UserToken, clip: any = {}): Promise => { const res = await api('clips/create', { description: null, isPublic: true, @@ -175,7 +177,7 @@ export const clip = async (user: any, clip: any = {}): Promise => { return res.body; }; -export const galleryPost = async (user: any, channel: any = {}): Promise => { +export const galleryPost = async (user: UserToken, channel: any = {}): Promise => { const res = await api('gallery/posts/create', { description: null, fileIds: [], @@ -186,7 +188,7 @@ export const galleryPost = async (user: any, channel: any = {}): Promise => return res.body; }; -export const channel = async (user: any, channel: any = {}): Promise => { +export const channel = async (user: UserToken, channel: any = {}): Promise => { const res = await api('channels/create', { bannerId: null, description: null, @@ -196,7 +198,7 @@ export const channel = async (user: any, channel: any = {}): Promise => { return res.body; }; -export const role = async (user: any, role: any = {}, policies: any = {}): Promise => { +export const role = async (user: UserToken, role: any = {}, policies: any = {}): Promise => { const res = await api('admin/roles/create', { asBadge: false, canEditMembersByModerator: false, @@ -239,7 +241,7 @@ interface UploadOptions { * Upload file * @param user User */ -export const uploadFile = async (user: any, { path, name, blob }: UploadOptions = {}): Promise => { +export const uploadFile = async (user: UserToken, { path, name, blob }: UploadOptions = {}): Promise => { const absPath = path == null ? new URL('resources/Lenna.jpg', import.meta.url) : isAbsolute(path.toString()) @@ -268,7 +270,7 @@ export const uploadFile = async (user: any, { path, name, blob }: UploadOptions }; }; -export const uploadUrl = async (user: any, url: string) => { +export const uploadUrl = async (user: UserToken, url: string) => { let file: any; const marker = Math.random().toString(); @@ -290,7 +292,7 @@ export const uploadUrl = async (user: any, url: string) => { return file; }; -export function connectStream(user: any, channel: string, listener: (message: Record) => any, params?: any): Promise { +export function connectStream(user: UserToken, channel: string, listener: (message: Record) => any, params?: any): Promise { return new Promise((res, rej) => { const ws = new WebSocket(`ws://127.0.0.1:${port}/streaming?i=${user.token}`); @@ -317,7 +319,7 @@ export function connectStream(user: any, channel: string, listener: (message: Re }); } -export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record) => boolean, params?: any) => { +export const waitFire = async (user: UserToken, channel: string, trgr: () => any, cond: (msg: Record) => boolean, params?: any) => { return new Promise(async (res, rej) => { let timer: NodeJS.Timeout | null = null; -- cgit v1.2.3-freya From 1b1f82a2e26ddabd8bdf400760a817acbf290157 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Wed, 28 Jun 2023 06:37:13 +0200 Subject: feat(backend): accept OAuth bearer token (#11052) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(backend): accept OAuth bearer token * refactor * Update packages/backend/src/server/api/ApiCallService.ts Co-authored-by: Acid Chicken (硫酸鶏) * Update packages/backend/src/server/api/ApiCallService.ts Co-authored-by: Acid Chicken (硫酸鶏) * fix * kind: permission for account moved error * also for suspended error * Update packages/backend/src/server/api/StreamingApiServerService.ts Co-authored-by: Acid Chicken (硫酸鶏) --------- Co-authored-by: Acid Chicken (硫酸鶏) Co-authored-by: syuilo --- packages/backend/src/server/api/ApiCallService.ts | 81 +++++++++++------- .../src/server/api/StreamingApiServerService.ts | 14 ++- packages/backend/test/e2e/api.ts | 99 +++++++++++++++++++++- packages/backend/test/utils.ts | 57 +++++++++---- packages/misskey-js/etc/misskey-js.api.md | 12 ++- packages/misskey-js/src/api.types.ts | 11 ++- 6 files changed, 222 insertions(+), 52 deletions(-) (limited to 'packages/backend/test/utils.ts') diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 45fb473763..09e3724394 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -53,44 +53,72 @@ export class ApiCallService implements OnApplicationShutdown { }, 1000 * 60 * 60); } + #sendApiError(reply: FastifyReply, err: ApiError): void { + let statusCode = err.httpStatusCode; + if (err.httpStatusCode === 401) { + reply.header('WWW-Authenticate', 'Bearer realm="Misskey"'); + } else if (err.kind === 'client') { + reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`); + statusCode = statusCode ?? 400; + } else if (err.kind === 'permission') { + // (ROLE_PERMISSION_DENIEDは関係ない) + if (err.code === 'PERMISSION_DENIED') { + reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`); + } + statusCode = statusCode ?? 403; + } else if (!statusCode) { + statusCode = 500; + } + this.send(reply, statusCode, err); + } + + #sendAuthenticationError(reply: FastifyReply, err: unknown): void { + if (err instanceof AuthenticationError) { + const message = 'Authentication failed. Please ensure your token is correct.'; + reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_token", error_description="${message}"`); + this.send(reply, 401, new ApiError({ + message: 'Authentication failed. Please ensure your token is correct.', + code: 'AUTHENTICATION_FAILED', + id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', + })); + } else { + this.send(reply, 500, new ApiError()); + } + } + @bindThis public handleRequest( endpoint: IEndpoint & { exec: any }, request: FastifyRequest<{ Body: Record | undefined, Querystring: Record }>, reply: FastifyReply, - ) { + ): void { const body = request.method === 'GET' ? request.query : request.body; - const token = body?.['i']; + // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive) + const token = request.headers.authorization?.startsWith('Bearer ') + ? request.headers.authorization.slice(7) + : body?.['i']; if (token != null && typeof token !== 'string') { reply.code(400); return; } this.authenticateService.authenticate(token).then(([user, app]) => { this.call(endpoint, user, app, body, null, request).then((res) => { - if (request.method === 'GET' && endpoint.meta.cacheSec && !body?.['i'] && !user) { + if (request.method === 'GET' && endpoint.meta.cacheSec && !token && !user) { reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`); } this.send(reply, res); }).catch((err: ApiError) => { - this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err); + this.#sendApiError(reply, err); }); if (user) { this.logIp(request, user); } }).catch(err => { - if (err instanceof AuthenticationError) { - this.send(reply, 401, new ApiError({ - message: 'Authentication failed. Please ensure your token is correct.', - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', - })); - } else { - this.send(reply, 500, new ApiError()); - } + this.#sendAuthenticationError(reply, err); }); } @@ -99,7 +127,7 @@ export class ApiCallService implements OnApplicationShutdown { endpoint: IEndpoint & { exec: any }, request: FastifyRequest<{ Body: Record, Querystring: Record }>, reply: FastifyReply, - ) { + ): Promise { const multipartData = await request.file().catch(() => { /* Fastify throws if the remote didn't send multipart data. Return 400 below. */ }); @@ -117,7 +145,10 @@ export class ApiCallService implements OnApplicationShutdown { fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined; } - const token = fields['i']; + // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive) + const token = request.headers.authorization?.startsWith('Bearer ') + ? request.headers.authorization.slice(7) + : fields['i']; if (token != null && typeof token !== 'string') { reply.code(400); return; @@ -129,22 +160,14 @@ export class ApiCallService implements OnApplicationShutdown { }, request).then((res) => { this.send(reply, res); }).catch((err: ApiError) => { - this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err); + this.#sendApiError(reply, err); }); if (user) { this.logIp(request, user); } }).catch(err => { - if (err instanceof AuthenticationError) { - this.send(reply, 401, new ApiError({ - message: 'Authentication failed. Please ensure your token is correct.', - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', - })); - } else { - this.send(reply, 500, new ApiError()); - } + this.#sendAuthenticationError(reply, err); }); } @@ -213,7 +236,7 @@ export class ApiCallService implements OnApplicationShutdown { } if (ep.meta.limit) { - // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. + // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. let limitActor: string; if (user) { limitActor = user.id; @@ -255,8 +278,8 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'Your account has been suspended.', code: 'YOUR_ACCOUNT_SUSPENDED', + kind: 'permission', id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370', - httpStatusCode: 403, }); } } @@ -266,8 +289,8 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'You have moved your account.', code: 'YOUR_ACCOUNT_MOVED', + kind: 'permission', id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31', - httpStatusCode: 403, }); } } @@ -321,7 +344,7 @@ export class ApiCallService implements OnApplicationShutdown { try { data[k] = JSON.parse(data[k]); } catch (e) { - throw new ApiError({ + throw new ApiError({ message: 'Invalid param.', code: 'INVALID_PARAM', id: '0b5f1631-7c1a-41a6-b399-cce335f34d85', diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 8f2e51d584..4a0342d2b4 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -58,11 +58,21 @@ export class StreamingApiServerService { let user: LocalUser | null = null; let app: AccessToken | null = null; + // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 + // Note that the standard WHATWG WebSocket API does not support setting any headers, + // but non-browser apps may still be able to set it. + const token = request.headers.authorization?.startsWith('Bearer ') + ? request.headers.authorization.slice(7) + : q.get('i'); + try { - [user, app] = await this.authenticateService.authenticate(q.get('i')); + [user, app] = await this.authenticateService.authenticate(token); } catch (e) { if (e instanceof AuthenticationError) { - socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.write([ + 'HTTP/1.1 401 Unauthorized', + 'WWW-Authenticate: Bearer realm="Misskey", error="invalid_token", error_description="Failed to authenticate"', + ].join('\r\n') + '\r\n\r\n'); } else { socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n'); } diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index 4b9167b6b1..c6beec4f88 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -1,9 +1,10 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, startServer, successfulApiCall, failedApiCall } from '../utils.js'; +import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; +import { IncomingMessage } from 'http'; describe('API', () => { let app: INestApplicationContext; @@ -123,4 +124,100 @@ describe('API', () => { id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', }); }); + + describe('Authentication header', () => { + test('一般リクエスト', async () => { + await successfulApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: { + token: alice.token, + bearer: true, + }, + }); + }); + + test('multipartリクエスト', async () => { + const result = await uploadFile({ + token: alice.token, + bearer: true, + }); + assert.strictEqual(result.status, 200); + }); + + test('streaming', async () => { + const fired = await waitFire( + { + token: alice.token, + bearer: true, + }, + 'homeTimeline', + () => api('notes/create', { text: 'foo' }, alice), + msg => msg.type === 'note' && msg.body.text === 'foo', + ); + assert.strictEqual(fired, true); + }); + }); + + describe('tokenエラー応答でWWW-Authenticate headerを送る', () => { + describe('invalid_token', () => { + test('一般リクエスト', async () => { + const result = await api('/admin/get-index-stats', {}, { + token: 'syuilo', + bearer: true, + }); + assert.strictEqual(result.status, 401); + assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_token", error_description')); + }); + + test('multipartリクエスト', async () => { + const result = await uploadFile({ + token: 'syuilo', + bearer: true, + }); + assert.strictEqual(result.status, 401); + assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_token", error_description')); + }); + + test('streaming', async () => { + await assert.rejects(connectStream( + { + token: 'syuilo', + bearer: true, + }, + 'homeTimeline', + () => { }, + ), (err: IncomingMessage) => { + assert.strictEqual(err.statusCode, 401); + assert.ok(err.headers['www-authenticate']?.startsWith('Bearer realm="Misskey", error="invalid_token", error_description')); + return true; + }); + }); + }); + + describe('tokenがないとrealmだけおくる', () => { + test('一般リクエスト', async () => { + const result = await api('/admin/get-index-stats', {}); + assert.strictEqual(result.status, 401); + assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"'); + }); + + test('multipartリクエスト', async () => { + const result = await uploadFile(); + assert.strictEqual(result.status, 401); + assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"'); + }); + }); + + test('invalid_request', async () => { + const result = await api('/notes/create', { text: true }, { + token: alice.token, + bearer: true, + }); + assert.strictEqual(result.status, 400); + assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_request", error_description')); + }); + + // TODO: insufficient_scope test (authテストが全然なくて書けない) + }); }); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 8583f024cb..48947072e3 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -2,7 +2,7 @@ import * as assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import { isAbsolute, basename } from 'node:path'; import { inspect } from 'node:util'; -import WebSocket from 'ws'; +import WebSocket, { ClientOptions } from 'ws'; import fetch, { Blob, File, RequestInit } from 'node-fetch'; import { DataSource } from 'typeorm'; import { JSDOM } from 'jsdom'; @@ -13,7 +13,10 @@ import type * as misskey from 'misskey-js'; export { server as startServer } from '@/boot/common.js'; -interface UserToken { token: string } +interface UserToken { + token: string; + bearer?: boolean; +} const config = loadConfig(); export const port = config.port; @@ -57,27 +60,33 @@ export const failedApiCall = async (request: ApiRequest, assertion: { return res.body; }; -const request = async (path: string, params: any, me?: UserToken): Promise<{ body: any, status: number }> => { - const auth = me ? { - i: me.token, - } : {}; +const request = async (path: string, params: any, me?: UserToken): Promise<{ status: number, headers: Headers, body: any }> => { + const bodyAuth: Record = {}; + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (me?.bearer) { + headers.Authorization = `Bearer ${me.token}`; + } else if (me) { + bodyAuth.i = me.token; + } const res = await relativeFetch(path, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(Object.assign(auth, params)), + headers, + body: JSON.stringify(Object.assign(bodyAuth, params)), redirect: 'manual', }); - const status = res.status; const body = res.headers.get('content-type') === 'application/json; charset=utf-8' ? await res.json() : null; return { - body, status, + status: res.status, + headers: res.headers, + body, }; }; @@ -241,7 +250,7 @@ interface UploadOptions { * Upload file * @param user User */ -export const uploadFile = async (user: UserToken, { path, name, blob }: UploadOptions = {}): Promise => { +export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ status: number, headers: Headers, body: misskey.Endpoints['drive/files/create']['res'] | null }> => { const absPath = path == null ? new URL('resources/Lenna.jpg', import.meta.url) : isAbsolute(path.toString()) @@ -249,7 +258,6 @@ export const uploadFile = async (user: UserToken, { path, name, blob }: UploadOp : new URL(path, new URL('resources/', import.meta.url)); const formData = new FormData(); - formData.append('i', user.token); formData.append('file', blob ?? new File([await readFile(absPath)], basename(absPath.toString()))); formData.append('force', 'true'); @@ -257,15 +265,24 @@ export const uploadFile = async (user: UserToken, { path, name, blob }: UploadOp formData.append('name', name); } + const headers: Record = {}; + if (user?.bearer) { + headers.Authorization = `Bearer ${user.token}`; + } else if (user) { + formData.append('i', user.token); + } + const res = await relativeFetch('api/drive/files/create', { method: 'POST', body: formData, + headers, }); - const body = res.status !== 204 ? await res.json() : null; + const body = res.status !== 204 ? await res.json() as misskey.Endpoints['drive/files/create']['res'] : null; return { status: res.status, + headers: res.headers, body, }; }; @@ -294,8 +311,16 @@ export const uploadUrl = async (user: UserToken, url: string) => { export function connectStream(user: UserToken, channel: string, listener: (message: Record) => any, params?: any): Promise { return new Promise((res, rej) => { - const ws = new WebSocket(`ws://127.0.0.1:${port}/streaming?i=${user.token}`); + const url = new URL(`ws://127.0.0.1:${port}/streaming`); + const options: ClientOptions = {}; + if (user.bearer) { + options.headers = { Authorization: `Bearer ${user.token}` }; + } else { + url.searchParams.set('i', user.token); + } + const ws = new WebSocket(url, options); + ws.on('unexpected-response', (req, res) => rej(res)); ws.on('open', () => { ws.on('message', data => { const msg = JSON.parse(data.toString()); diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 5f292148ae..c9b3fd6056 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -960,8 +960,14 @@ export type Endpoints = { res: TODO; }; 'drive/files/create': { - req: TODO; - res: TODO; + req: { + folderId?: string; + name?: string; + comment?: string; + isSentisive?: boolean; + force?: boolean; + }; + res: DriveFile; }; 'drive/files/delete': { req: { @@ -2750,7 +2756,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts -// src/api.types.ts:611:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts +// src/api.types.ts:620:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index 293e0043b7..93f327e67e 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -262,7 +262,16 @@ export type Endpoints = { 'drive/files': { req: { folderId?: DriveFolder['id'] | null; type?: DriveFile['type'] | null; limit?: number; sinceId?: DriveFile['id']; untilId?: DriveFile['id']; }; res: DriveFile[]; }; 'drive/files/attached-notes': { req: TODO; res: TODO; }; 'drive/files/check-existence': { req: TODO; res: TODO; }; - 'drive/files/create': { req: TODO; res: TODO; }; + 'drive/files/create': { + req: { + folderId?: string, + name?: string, + comment?: string, + isSentisive?: boolean, + force?: boolean, + }; + res: DriveFile; + }; 'drive/files/delete': { req: { fileId: DriveFile['id']; }; res: null; }; 'drive/files/find-by-hash': { req: TODO; res: TODO; }; 'drive/files/find': { req: { name: string; folderId?: DriveFolder['id'] | null; }; res: DriveFile[]; }; -- cgit v1.2.3-freya From 2b6dbd4fcbec380d65b0c318932d9eeb3fcb3f7b Mon Sep 17 00:00:00 2001 From: okayurisotto Date: Fri, 14 Jul 2023 10:45:01 +0900 Subject: refactor: 可読性のため一部で`Array.prototype.at`を使うように (#11274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: `Array.prototype.at`を使うように * fixup! refactor: `Array.prototype.at`を使うように --- packages/backend/src/core/chart/core.ts | 2 +- packages/backend/src/misc/prelude/array.ts | 5 +++-- .../src/queue/processors/CleanRemoteFilesProcessorService.ts | 6 +++--- .../backend/src/queue/processors/DeleteAccountProcessorService.ts | 4 ++-- .../src/queue/processors/DeleteDriveFilesProcessorService.ts | 6 +++--- .../src/queue/processors/ExportBlockingProcessorService.ts | 6 +++--- .../src/queue/processors/ExportFavoritesProcessorService.ts | 2 +- .../src/queue/processors/ExportFollowingProcessorService.ts | 2 +- .../backend/src/queue/processors/ExportMutingProcessorService.ts | 6 +++--- .../backend/src/queue/processors/ExportNotesProcessorService.ts | 2 +- packages/backend/src/server/ActivityPubServerService.ts | 6 +++--- packages/backend/test/utils.ts | 8 ++++---- packages/frontend/src/components/MkDrive.vue | 4 ++-- packages/frontend/src/components/MkMiniChart.vue | 4 ++-- packages/frontend/src/components/MkPageWindow.vue | 2 +- packages/frontend/src/components/MkPagination.vue | 4 ++-- packages/frontend/src/scripts/array.ts | 5 +++-- packages/frontend/src/widgets/server-metric/cpu-mem.vue | 8 ++++---- packages/frontend/src/widgets/server-metric/net.vue | 8 ++++---- 19 files changed, 46 insertions(+), 44 deletions(-) (limited to 'packages/backend/test/utils.ts') diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts index 5717024351..7a89233eda 100644 --- a/packages/backend/src/core/chart/core.ts +++ b/packages/backend/src/core/chart/core.ts @@ -627,7 +627,7 @@ export default abstract class Chart { } // 要求された範囲の最も古い箇所に位置するログが存在しなかったら - } else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) { + } else if (!isTimeSame(new Date(logs.at(-1)!.date * 1000), gt)) { // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する // (隙間埋めできないため) const outdatedLog = await repository.findOne({ diff --git a/packages/backend/src/misc/prelude/array.ts b/packages/backend/src/misc/prelude/array.ts index 0b2830cb7b..2524eacfb3 100644 --- a/packages/backend/src/misc/prelude/array.ts +++ b/packages/backend/src/misc/prelude/array.ts @@ -67,8 +67,9 @@ export function maximum(xs: number[]): number { export function groupBy(f: EndoRelation, xs: T[]): T[][] { const groups = [] as T[][]; for (const x of xs) { - if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) { - groups[groups.length - 1].push(x); + const lastGroup = groups.at(-1); + if (lastGroup !== undefined && f(lastGroup[0], x)) { + lastGroup.push(x); } else { groups.push([x]); } diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts index c54bf59ae4..6f887089eb 100644 --- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFile, DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; @@ -31,7 +31,7 @@ export class CleanRemoteFilesProcessorService { this.logger.info('Deleting cached remote files...'); let deletedCount = 0; - let cursor: any = null; + let cursor: DriveFile['id'] | null = null; while (true) { const files = await this.driveFilesRepository.find({ @@ -51,7 +51,7 @@ export class CleanRemoteFilesProcessorService { break; } - cursor = files[files.length - 1].id; + cursor = files.at(-1)?.id ?? null; await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true))); diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index 65ded170b7..b2886563f4 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -70,7 +70,7 @@ export class DeleteAccountProcessorService { break; } - cursor = notes[notes.length - 1].id; + cursor = notes.at(-1)?.id ?? null; await this.notesRepository.delete(notes.map(note => note.id)); @@ -101,7 +101,7 @@ export class DeleteAccountProcessorService { break; } - cursor = files[files.length - 1].id; + cursor = files.at(-1)?.id ?? null; for (const file of files) { await this.driveService.deleteFileSync(file); diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts index 6772c5dc76..07e3762330 100644 --- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; +import type { UsersRepository, DriveFilesRepository, DriveFile } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; @@ -40,7 +40,7 @@ export class DeleteDriveFilesProcessorService { } let deletedCount = 0; - let cursor: any = null; + let cursor: DriveFile['id'] | null = null; while (true) { const files = await this.driveFilesRepository.find({ @@ -59,7 +59,7 @@ export class DeleteDriveFilesProcessorService { break; } - cursor = files[files.length - 1].id; + cursor = files.at(-1)?.id ?? null; for (const file of files) { await this.driveService.deleteFileSync(file); diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index eb758e162d..d100c6d09f 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, BlockingsRepository } from '@/models/index.js'; +import type { UsersRepository, BlockingsRepository, Blocking } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; @@ -53,7 +53,7 @@ export class ExportBlockingProcessorService { const stream = fs.createWriteStream(path, { flags: 'a' }); let exportedCount = 0; - let cursor: any = null; + let cursor: Blocking['id'] | null = null; while (true) { const blockings = await this.blockingsRepository.find({ @@ -72,7 +72,7 @@ export class ExportBlockingProcessorService { break; } - cursor = blockings[blockings.length - 1].id; + cursor = blockings.at(-1)?.id ?? null; for (const block of blockings) { const u = await this.usersRepository.findOneBy({ id: block.blockeeId }); diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index 76c38a6b86..2be42b1a7a 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -94,7 +94,7 @@ export class ExportFavoritesProcessorService { break; } - cursor = favorites[favorites.length - 1].id; + cursor = favorites.at(-1)?.id ?? null; for (const favorite of favorites) { let poll: Poll | undefined; diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts index 8726cb1402..d54e5e0b34 100644 --- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -79,7 +79,7 @@ export class ExportFollowingProcessorService { break; } - cursor = followings[followings.length - 1].id; + cursor = followings.at(-1)?.id ?? null; for (const following of followings) { const u = await this.usersRepository.findOneBy({ id: following.followeeId }); diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts index 0f11a9e843..030e38931e 100644 --- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository, UsersRepository, BlockingsRepository } from '@/models/index.js'; +import type { MutingsRepository, UsersRepository, BlockingsRepository, Muting } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; @@ -56,7 +56,7 @@ export class ExportMutingProcessorService { const stream = fs.createWriteStream(path, { flags: 'a' }); let exportedCount = 0; - let cursor: any = null; + let cursor: Muting['id'] | null = null; while (true) { const mutes = await this.mutingsRepository.find({ @@ -76,7 +76,7 @@ export class ExportMutingProcessorService { break; } - cursor = mutes[mutes.length - 1].id; + cursor = mutes.at(-1)?.id ?? null; for (const mute of mutes) { const u = await this.usersRepository.findOneBy({ id: mute.muteeId }); diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index 24fb331883..75f32ffee3 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -90,7 +90,7 @@ export class ExportNotesProcessorService { break; } - cursor = notes[notes.length - 1].id; + cursor = notes.at(-1)?.id ?? null; for (const note of notes) { let poll: Poll | undefined; diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index f751709345..634f5f0a4e 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -181,7 +181,7 @@ export class ActivityPubServerService { undefined, inStock ? `${partOf}?${url.query({ page: 'true', - cursor: followings[followings.length - 1].id, + cursor: followings.at(-1)!.id, })}` : undefined, ); @@ -273,7 +273,7 @@ export class ActivityPubServerService { undefined, inStock ? `${partOf}?${url.query({ page: 'true', - cursor: followings[followings.length - 1].id, + cursor: followings.at(-1)!.id, })}` : undefined, ); @@ -398,7 +398,7 @@ export class ActivityPubServerService { })}` : undefined, notes.length ? `${partOf}?${url.query({ page: 'true', - until_id: notes[notes.length - 1].id, + until_id: notes.at(-1)!.id, })}` : undefined, ); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 48947072e3..31ea3e5ab8 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -447,12 +447,12 @@ export async function testPaginationConsistency id + ':' + createdAt), @@ -480,7 +480,7 @@ export async function testPaginationConsistency id + ':' + createdAt), diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 201a6ccdc8..aff227da40 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -568,7 +568,7 @@ function fetchMoreFolders() { os.api('drive/folders', { folderId: folder.value ? folder.value.id : null, type: props.type, - untilId: folders.value[folders.value.length - 1].id, + untilId: folders.value.at(-1)?.id, limit: max + 1, }).then(folders => { if (folders.length === max + 1) { @@ -591,7 +591,7 @@ function fetchMoreFiles() { os.api('drive/files', { folderId: folder.value ? folder.value.id : null, type: props.type, - untilId: files.value[files.value.length - 1].id, + untilId: files.value.at(-1)?.id, limit: max + 1, }).then(files => { if (files.length === max + 1) { diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue index 89050e10f0..e884455709 100644 --- a/packages/frontend/src/components/MkMiniChart.vue +++ b/packages/frontend/src/components/MkMiniChart.vue @@ -59,8 +59,8 @@ function draw(): void { polygonPoints = `0,${ viewBoxY } ${ polylinePoints } ${ viewBoxX },${ viewBoxY }`; - headX = _polylinePoints[_polylinePoints.length - 1][0]; - headY = _polylinePoints[_polylinePoints.length - 1][1]; + headX = _polylinePoints.at(-1)![0]; + headY = _polylinePoints.at(-1)![1]; } watch(() => props.src, draw, { immediate: true }); diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 6318a9fd70..6e35ad4241 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -120,7 +120,7 @@ const contextmenu = $computed(() => ([{ function back() { history.pop(); - router.replace(history[history.length - 1].path, history[history.length - 1].key); + router.replace(history.at(-1)!.path, history.at(-1)!.key); } function reload() { diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 661b04c365..b9a75f6002 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -233,7 +233,7 @@ const fetchMore = async (): Promise => { ...(props.pagination.offsetMode ? { offset: offset.value, } : { - untilId: Array.from(items.value.keys())[items.value.size - 1], + untilId: Array.from(items.value.keys()).at(-1), }), }).then(res => { for (let i = 0; i < res.length; i++) { @@ -297,7 +297,7 @@ const fetchMoreAhead = async (): Promise => { ...(props.pagination.offsetMode ? { offset: offset.value, } : { - sinceId: Array.from(items.value.keys())[items.value.size - 1], + sinceId: Array.from(items.value.keys()).at(-1), }), }).then(res => { if (res.length === 0) { diff --git a/packages/frontend/src/scripts/array.ts b/packages/frontend/src/scripts/array.ts index 4620c8b735..c9a146e707 100644 --- a/packages/frontend/src/scripts/array.ts +++ b/packages/frontend/src/scripts/array.ts @@ -78,8 +78,9 @@ export function maximum(xs: number[]): number { export function groupBy(f: EndoRelation, xs: T[]): T[][] { const groups = [] as T[][]; for (const x of xs) { - if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) { - groups[groups.length - 1].push(x); + const lastGroup = groups.at(-1); + if (lastGroup !== undefined && f(lastGroup[0], x)) { + lastGroup.push(x); } else { groups.push([x]); } diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue index c178ba5171..b9ba400b4d 100644 --- a/packages/frontend/src/widgets/server-metric/cpu-mem.vue +++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue @@ -121,10 +121,10 @@ function onStats(connStats) { cpuPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${cpuPolylinePoints} ${viewBoxX},${viewBoxY}`; memPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${memPolylinePoints} ${viewBoxX},${viewBoxY}`; - cpuHeadX = cpuPolylinePointsStats[cpuPolylinePointsStats.length - 1][0]; - cpuHeadY = cpuPolylinePointsStats[cpuPolylinePointsStats.length - 1][1]; - memHeadX = memPolylinePointsStats[memPolylinePointsStats.length - 1][0]; - memHeadY = memPolylinePointsStats[memPolylinePointsStats.length - 1][1]; + cpuHeadX = cpuPolylinePointsStats.at(-1)![0]; + cpuHeadY = cpuPolylinePointsStats.at(-1)![1]; + memHeadX = memPolylinePointsStats.at(-1)![0]; + memHeadY = memPolylinePointsStats.at(-1)![1]; cpuP = (connStats.cpu * 100).toFixed(0); memP = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0); diff --git a/packages/frontend/src/widgets/server-metric/net.vue b/packages/frontend/src/widgets/server-metric/net.vue index 5a9134078d..817a422e63 100644 --- a/packages/frontend/src/widgets/server-metric/net.vue +++ b/packages/frontend/src/widgets/server-metric/net.vue @@ -94,10 +94,10 @@ function onStats(connStats) { inPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${inPolylinePoints} ${viewBoxX},${viewBoxY}`; outPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${outPolylinePoints} ${viewBoxX},${viewBoxY}`; - inHeadX = inPolylinePointsStats[inPolylinePointsStats.length - 1][0]; - inHeadY = inPolylinePointsStats[inPolylinePointsStats.length - 1][1]; - outHeadX = outPolylinePointsStats[outPolylinePointsStats.length - 1][0]; - outHeadY = outPolylinePointsStats[outPolylinePointsStats.length - 1][1]; + inHeadX = inPolylinePointsStats.at(-1)![0]; + inHeadY = inPolylinePointsStats.at(-1)![1]; + outHeadX = outPolylinePointsStats.at(-1)![0]; + outHeadY = outPolylinePointsStats.at(-1)![1]; inRecent = connStats.net.rx; outRecent = connStats.net.tx; -- cgit v1.2.3-freya