feat(backend): accept OAuth bearer token (#11052)
* feat(backend): accept OAuth bearer token * refactor * Update packages/backend/src/server/api/ApiCallService.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * Update packages/backend/src/server/api/ApiCallService.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * fix * kind: permission for account moved error * also for suspended error * Update packages/backend/src/server/api/StreamingApiServerService.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> --------- Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
parent
d48172e9d1
commit
1b1f82a2e2
|
@ -53,44 +53,72 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
}, 1000 * 60 * 60);
|
}, 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
|
@bindThis
|
||||||
public handleRequest(
|
public handleRequest(
|
||||||
endpoint: IEndpoint & { exec: any },
|
endpoint: IEndpoint & { exec: any },
|
||||||
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) {
|
): void {
|
||||||
const body = request.method === 'GET'
|
const body = request.method === 'GET'
|
||||||
? request.query
|
? request.query
|
||||||
: request.body;
|
: 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') {
|
if (token != null && typeof token !== 'string') {
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.authenticateService.authenticate(token).then(([user, app]) => {
|
this.authenticateService.authenticate(token).then(([user, app]) => {
|
||||||
this.call(endpoint, user, app, body, null, request).then((res) => {
|
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}`);
|
reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
|
||||||
}
|
}
|
||||||
this.send(reply, res);
|
this.send(reply, res);
|
||||||
}).catch((err: ApiError) => {
|
}).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) {
|
if (user) {
|
||||||
this.logIp(request, user);
|
this.logIp(request, user);
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (err instanceof AuthenticationError) {
|
this.#sendAuthenticationError(reply, err);
|
||||||
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());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +127,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
endpoint: IEndpoint & { exec: any },
|
endpoint: IEndpoint & { exec: any },
|
||||||
request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
|
request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) {
|
): Promise<void> {
|
||||||
const multipartData = await request.file().catch(() => {
|
const multipartData = await request.file().catch(() => {
|
||||||
/* Fastify throws if the remote didn't send multipart data. Return 400 below. */
|
/* 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;
|
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') {
|
if (token != null && typeof token !== 'string') {
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
|
@ -129,22 +160,14 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
}, request).then((res) => {
|
}, request).then((res) => {
|
||||||
this.send(reply, res);
|
this.send(reply, res);
|
||||||
}).catch((err: ApiError) => {
|
}).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) {
|
if (user) {
|
||||||
this.logIp(request, user);
|
this.logIp(request, user);
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (err instanceof AuthenticationError) {
|
this.#sendAuthenticationError(reply, err);
|
||||||
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());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +236,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ep.meta.limit) {
|
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;
|
let limitActor: string;
|
||||||
if (user) {
|
if (user) {
|
||||||
limitActor = user.id;
|
limitActor = user.id;
|
||||||
|
@ -255,8 +278,8 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
throw new ApiError({
|
throw new ApiError({
|
||||||
message: 'Your account has been suspended.',
|
message: 'Your account has been suspended.',
|
||||||
code: 'YOUR_ACCOUNT_SUSPENDED',
|
code: 'YOUR_ACCOUNT_SUSPENDED',
|
||||||
|
kind: 'permission',
|
||||||
id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370',
|
id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370',
|
||||||
httpStatusCode: 403,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -266,8 +289,8 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
throw new ApiError({
|
throw new ApiError({
|
||||||
message: 'You have moved your account.',
|
message: 'You have moved your account.',
|
||||||
code: 'YOUR_ACCOUNT_MOVED',
|
code: 'YOUR_ACCOUNT_MOVED',
|
||||||
|
kind: 'permission',
|
||||||
id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31',
|
id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31',
|
||||||
httpStatusCode: 403,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -321,7 +344,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
try {
|
try {
|
||||||
data[k] = JSON.parse(data[k]);
|
data[k] = JSON.parse(data[k]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new ApiError({
|
throw new ApiError({
|
||||||
message: 'Invalid param.',
|
message: 'Invalid param.',
|
||||||
code: 'INVALID_PARAM',
|
code: 'INVALID_PARAM',
|
||||||
id: '0b5f1631-7c1a-41a6-b399-cce335f34d85',
|
id: '0b5f1631-7c1a-41a6-b399-cce335f34d85',
|
||||||
|
|
|
@ -58,11 +58,21 @@ export class StreamingApiServerService {
|
||||||
let user: LocalUser | null = null;
|
let user: LocalUser | null = null;
|
||||||
let app: AccessToken | 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 {
|
try {
|
||||||
[user, app] = await this.authenticateService.authenticate(q.get('i'));
|
[user, app] = await this.authenticateService.authenticate(token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof AuthenticationError) {
|
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 {
|
} else {
|
||||||
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
|
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
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 { INestApplicationContext } from '@nestjs/common';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
import { IncomingMessage } from 'http';
|
||||||
|
|
||||||
describe('API', () => {
|
describe('API', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
|
@ -123,4 +124,100 @@ describe('API', () => {
|
||||||
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
|
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 { readFile } from 'node:fs/promises';
|
||||||
import { isAbsolute, basename } from 'node:path';
|
import { isAbsolute, basename } from 'node:path';
|
||||||
import { inspect } from 'node:util';
|
import { inspect } from 'node:util';
|
||||||
import WebSocket from 'ws';
|
import WebSocket, { ClientOptions } from 'ws';
|
||||||
import fetch, { Blob, File, RequestInit } from 'node-fetch';
|
import fetch, { Blob, File, RequestInit } from 'node-fetch';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
|
@ -13,7 +13,10 @@ import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
export { server as startServer } from '@/boot/common.js';
|
export { server as startServer } from '@/boot/common.js';
|
||||||
|
|
||||||
interface UserToken { token: string }
|
interface UserToken {
|
||||||
|
token: string;
|
||||||
|
bearer?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
export const port = config.port;
|
export const port = config.port;
|
||||||
|
@ -57,27 +60,33 @@ export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
|
||||||
return res.body;
|
return res.body;
|
||||||
};
|
};
|
||||||
|
|
||||||
const request = async (path: string, params: any, me?: UserToken): Promise<{ body: any, status: number }> => {
|
const request = async (path: string, params: any, me?: UserToken): Promise<{ status: number, headers: Headers, body: any }> => {
|
||||||
const auth = me ? {
|
const bodyAuth: Record<string, string> = {};
|
||||||
i: me.token,
|
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, {
|
const res = await relativeFetch(path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers,
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify(Object.assign(bodyAuth, params)),
|
||||||
},
|
|
||||||
body: JSON.stringify(Object.assign(auth, params)),
|
|
||||||
redirect: 'manual',
|
redirect: 'manual',
|
||||||
});
|
});
|
||||||
|
|
||||||
const status = res.status;
|
|
||||||
const body = res.headers.get('content-type') === 'application/json; charset=utf-8'
|
const body = res.headers.get('content-type') === 'application/json; charset=utf-8'
|
||||||
? await res.json()
|
? await res.json()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
body, status,
|
status: res.status,
|
||||||
|
headers: res.headers,
|
||||||
|
body,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -241,7 +250,7 @@ interface UploadOptions {
|
||||||
* Upload file
|
* Upload file
|
||||||
* @param user User
|
* @param user User
|
||||||
*/
|
*/
|
||||||
export const uploadFile = async (user: UserToken, { 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
|
const absPath = path == null
|
||||||
? new URL('resources/Lenna.jpg', import.meta.url)
|
? new URL('resources/Lenna.jpg', import.meta.url)
|
||||||
: isAbsolute(path.toString())
|
: 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));
|
: new URL(path, new URL('resources/', import.meta.url));
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('i', user.token);
|
|
||||||
formData.append('file', blob ??
|
formData.append('file', blob ??
|
||||||
new File([await readFile(absPath)], basename(absPath.toString())));
|
new File([await readFile(absPath)], basename(absPath.toString())));
|
||||||
formData.append('force', 'true');
|
formData.append('force', 'true');
|
||||||
|
@ -257,15 +265,24 @@ export const uploadFile = async (user: UserToken, { path, name, blob }: UploadOp
|
||||||
formData.append('name', name);
|
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', {
|
const res = await relativeFetch('api/drive/files/create', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
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 {
|
return {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
|
headers: res.headers,
|
||||||
body,
|
body,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -294,8 +311,16 @@ export const uploadUrl = async (user: UserToken, url: string) => {
|
||||||
|
|
||||||
export function connectStream(user: UserToken, 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) => {
|
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('open', () => {
|
||||||
ws.on('message', data => {
|
ws.on('message', data => {
|
||||||
const msg = JSON.parse(data.toString());
|
const msg = JSON.parse(data.toString());
|
||||||
|
|
|
@ -960,8 +960,14 @@ export type Endpoints = {
|
||||||
res: TODO;
|
res: TODO;
|
||||||
};
|
};
|
||||||
'drive/files/create': {
|
'drive/files/create': {
|
||||||
req: TODO;
|
req: {
|
||||||
res: TODO;
|
folderId?: string;
|
||||||
|
name?: string;
|
||||||
|
comment?: string;
|
||||||
|
isSentisive?: boolean;
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
res: DriveFile;
|
||||||
};
|
};
|
||||||
'drive/files/delete': {
|
'drive/files/delete': {
|
||||||
req: {
|
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: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: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
|
// 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)
|
// (No @packageDocumentation comment for this package)
|
||||||
|
|
|
@ -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': { 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/attached-notes': { req: TODO; res: TODO; };
|
||||||
'drive/files/check-existence': { 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/delete': { req: { fileId: DriveFile['id']; }; res: null; };
|
||||||
'drive/files/find-by-hash': { req: TODO; res: TODO; };
|
'drive/files/find-by-hash': { req: TODO; res: TODO; };
|
||||||
'drive/files/find': { req: { name: string; folderId?: DriveFolder['id'] | null; }; res: DriveFile[]; };
|
'drive/files/find': { req: { name: string; folderId?: DriveFolder['id'] | null; }; res: DriveFile[]; };
|
||||||
|
|
Loading…
Reference in a new issue