Merge remote-tracking branch 'upstream/develop' into abuse-report-resolver
This commit is contained in:
		
						commit
						cf0b6ff2bc
					
				|  | @ -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<string, unknown> | undefined, Querystring: Record<string, unknown> }>, | ||||
| 		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<string, unknown>, Querystring: Record<string, unknown> }>, | ||||
| 		reply: FastifyReply, | ||||
| 	) { | ||||
| 	): Promise<void> { | ||||
| 		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', | ||||
|  |  | |||
|  | @ -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'); | ||||
| 				} | ||||
|  |  | |||
|  | @ -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テストが全然なくて書けない)
 | ||||
| 	}); | ||||
| }); | ||||
|  |  | |||
|  | @ -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,14 +13,19 @@ import type * as misskey from 'misskey-js'; | |||
| 
 | ||||
| export { server as startServer } from '@/boot/common.js'; | ||||
| 
 | ||||
| interface UserToken { | ||||
| 	token: string; | ||||
| 	bearer?: boolean; | ||||
| } | ||||
| 
 | ||||
| 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 +33,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 <T, >(request: ApiRequest, assertion: { | ||||
|  | @ -55,27 +60,33 @@ export const failedApiCall = async <T, >(request: ApiRequest, assertion: { | |||
| 	return res.body; | ||||
| }; | ||||
| 
 | ||||
| const request = async (path: string, params: any, me?: any): 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<string, string> = {}; | ||||
| 	const headers: Record<string, string> = { | ||||
| 		'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, | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
|  | @ -94,7 +105,7 @@ export const signup = async (params?: Partial<misskey.Endpoints['signup']['req'] | |||
| 	return res.body; | ||||
| }; | ||||
| 
 | ||||
| export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => { | ||||
| export const post = async (user: UserToken, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => { | ||||
| 	const q = params; | ||||
| 
 | ||||
| 	const res = await api('notes/create', q, user); | ||||
|  | @ -117,21 +128,21 @@ export const hiddenNote = (note: any): any => { | |||
| 	return temp; | ||||
| }; | ||||
| 
 | ||||
| export const react = async (user: any, note: any, reaction: string): Promise<any> => { | ||||
| export const react = async (user: UserToken, note: any, reaction: string): Promise<any> => { | ||||
| 	await api('notes/reactions/create', { | ||||
| 		noteId: note.id, | ||||
| 		reaction: reaction, | ||||
| 	}, user); | ||||
| }; | ||||
| 
 | ||||
| export const userList = async (user: any, userList: any = {}): Promise<any> => { | ||||
| export const userList = async (user: UserToken, userList: any = {}): Promise<any> => { | ||||
| 	const res = await api('users/lists/create', { | ||||
| 		name: 'test', | ||||
| 	}, user); | ||||
| 	return res.body; | ||||
| }; | ||||
| 
 | ||||
| export const page = async (user: any, page: any = {}): Promise<any> => { | ||||
| export const page = async (user: UserToken, page: any = {}): Promise<any> => { | ||||
| 	const res = await api('pages/create', { | ||||
| 		alignCenter: false, | ||||
| 		content: [ | ||||
|  | @ -154,7 +165,7 @@ export const page = async (user: any, page: any = {}): Promise<any> => { | |||
| 	return res.body; | ||||
| }; | ||||
| 
 | ||||
| export const play = async (user: any, play: any = {}): Promise<any> => { | ||||
| export const play = async (user: UserToken, play: any = {}): Promise<any> => { | ||||
| 	const res = await api('flash/create', { | ||||
| 		permissions: [], | ||||
| 		script: 'test', | ||||
|  | @ -165,7 +176,7 @@ export const play = async (user: any, play: any = {}): Promise<any> => { | |||
| 	return res.body; | ||||
| }; | ||||
| 
 | ||||
| export const clip = async (user: any, clip: any = {}): Promise<any> => { | ||||
| export const clip = async (user: UserToken, clip: any = {}): Promise<any> => { | ||||
| 	const res = await api('clips/create', { | ||||
| 		description: null, | ||||
| 		isPublic: true, | ||||
|  | @ -175,7 +186,7 @@ export const clip = async (user: any, clip: any = {}): Promise<any> => { | |||
| 	return res.body; | ||||
| }; | ||||
| 
 | ||||
| export const galleryPost = async (user: any, channel: any = {}): Promise<any> => { | ||||
| export const galleryPost = async (user: UserToken, channel: any = {}): Promise<any> => { | ||||
| 	const res = await api('gallery/posts/create', { | ||||
| 		description: null, | ||||
| 		fileIds: [], | ||||
|  | @ -186,7 +197,7 @@ export const galleryPost = async (user: any, channel: any = {}): Promise<any> => | |||
| 	return res.body; | ||||
| }; | ||||
| 
 | ||||
| export const channel = async (user: any, channel: any = {}): Promise<any> => { | ||||
| export const channel = async (user: UserToken, channel: any = {}): Promise<any> => { | ||||
| 	const res = await api('channels/create', { | ||||
| 		bannerId: null, | ||||
| 		description: null, | ||||
|  | @ -196,7 +207,7 @@ export const channel = async (user: any, channel: any = {}): Promise<any> => { | |||
| 	return res.body; | ||||
| }; | ||||
| 
 | ||||
| export const role = async (user: any, role: any = {}, policies: any = {}): Promise<any> => { | ||||
| export const role = async (user: UserToken, role: any = {}, policies: any = {}): Promise<any> => { | ||||
| 	const res = await api('admin/roles/create', { | ||||
| 		asBadge: false, | ||||
| 		canEditMembersByModerator: false, | ||||
|  | @ -239,7 +250,7 @@ interface UploadOptions { | |||
|  * Upload file | ||||
|  * @param user User | ||||
|  */ | ||||
| export const uploadFile = async (user: any, { path, name, blob }: UploadOptions = {}): Promise<any> => { | ||||
| 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()) | ||||
|  | @ -247,7 +258,6 @@ export const uploadFile = async (user: any, { path, name, blob }: UploadOptions | |||
| 			: 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'); | ||||
|  | @ -255,20 +265,29 @@ export const uploadFile = async (user: any, { path, name, blob }: UploadOptions | |||
| 		formData.append('name', name); | ||||
| 	} | ||||
| 
 | ||||
| 	const headers: Record<string, string> = {}; | ||||
| 	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, | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
| 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,10 +309,18 @@ export const uploadUrl = async (user: any, url: string) => { | |||
| 	return file; | ||||
| }; | ||||
| 
 | ||||
| export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> { | ||||
| export function connectStream(user: UserToken, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> { | ||||
| 	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()); | ||||
|  | @ -317,7 +344,7 @@ export function connectStream(user: any, channel: string, listener: (message: Re | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean, params?: any) => { | ||||
| export const waitFire = async (user: UserToken, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean, params?: any) => { | ||||
| 	return new Promise<boolean>(async (res, rej) => { | ||||
| 		let timer: NodeJS.Timeout | null = null; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| const canvas = new OffscreenCanvas(1, 1); | ||||
| const gl = canvas.getContext('webgl2'); | ||||
| const canvas = globalThis.OffscreenCanvas && new OffscreenCanvas(1, 1); | ||||
| const gl = canvas?.getContext('webgl2'); | ||||
| if (gl) { | ||||
|     postMessage({ result: true }); | ||||
| } else { | ||||
|  |  | |||
|  | @ -976,8 +976,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: { | ||||
|  | @ -2766,7 +2772,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:615:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts | ||||
| // src/api.types.ts:624: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) | ||||
|  |  | |||
|  | @ -266,7 +266,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[]; }; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue