2023-03-21 16:59:50 -07:00
|
|
|
|
import * as assert from 'node:assert';
|
2023-03-02 18:13:12 -08:00
|
|
|
|
import { readFile } from 'node:fs/promises';
|
|
|
|
|
import { isAbsolute, basename } from 'node:path';
|
2023-03-19 04:26:38 -07:00
|
|
|
|
import { inspect } from 'node:util';
|
2023-03-02 18:13:12 -08:00
|
|
|
|
import WebSocket from 'ws';
|
|
|
|
|
import fetch, { Blob, File, RequestInit } from 'node-fetch';
|
|
|
|
|
import { DataSource } from 'typeorm';
|
2023-03-17 17:01:10 -07:00
|
|
|
|
import { JSDOM } from 'jsdom';
|
2023-04-11 21:20:16 -07:00
|
|
|
|
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
2023-03-02 18:13:12 -08:00
|
|
|
|
import { entities } from '../src/postgres.js';
|
|
|
|
|
import { loadConfig } from '../src/config.js';
|
|
|
|
|
import type * as misskey from 'misskey-js';
|
|
|
|
|
|
|
|
|
|
export { server as startServer } from '@/boot/common.js';
|
|
|
|
|
|
|
|
|
|
const config = loadConfig();
|
|
|
|
|
export const port = config.port;
|
|
|
|
|
|
2023-03-17 17:01:10 -07:00
|
|
|
|
export const cookie = (me: any): string => {
|
|
|
|
|
return `token=${me.token};`;
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-02 18:13:12 -08:00
|
|
|
|
export const api = async (endpoint: string, params: any, me?: any) => {
|
|
|
|
|
const normalized = endpoint.replace(/^\//, '');
|
|
|
|
|
return await request(`api/${normalized}`, params, me);
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-19 04:26:38 -07:00
|
|
|
|
export type ApiRequest = {
|
|
|
|
|
endpoint: string,
|
|
|
|
|
parameters: object,
|
|
|
|
|
user: object | undefined,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const successfulApiCall = async <T, >(request: ApiRequest, assertion: {
|
2023-04-11 21:20:16 -07:00
|
|
|
|
status?: number,
|
|
|
|
|
} = {}): Promise<T> => {
|
2023-03-19 04:26:38 -07:00
|
|
|
|
const { endpoint, parameters, user } = request;
|
|
|
|
|
const res = await api(endpoint, parameters, user);
|
2023-04-11 21:20:16 -07:00
|
|
|
|
const status = assertion.status ?? (res.body == null ? 204 : 200);
|
|
|
|
|
assert.strictEqual(res.status, status, inspect(res.body, { depth: 5, colors: true }));
|
2023-03-19 04:26:38 -07:00
|
|
|
|
return res.body;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
|
|
|
|
|
status: number,
|
|
|
|
|
code: string,
|
|
|
|
|
id: string
|
|
|
|
|
}): Promise<T> => {
|
|
|
|
|
const { endpoint, parameters, user } = request;
|
|
|
|
|
const { status, code, id } = assertion;
|
|
|
|
|
const res = await api(endpoint, parameters, user);
|
|
|
|
|
assert.strictEqual(res.status, status, inspect(res.body));
|
|
|
|
|
assert.strictEqual(res.body.error.code, code, inspect(res.body));
|
|
|
|
|
assert.strictEqual(res.body.error.id, id, inspect(res.body));
|
|
|
|
|
return res.body;
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-02 18:13:12 -08:00
|
|
|
|
const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
|
|
|
|
|
const auth = me ? {
|
|
|
|
|
i: me.token,
|
|
|
|
|
} : {};
|
|
|
|
|
|
|
|
|
|
const res = await relativeFetch(path, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(Object.assign(auth, 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,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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<any> => {
|
|
|
|
|
const q = Object.assign({
|
|
|
|
|
username: 'test',
|
|
|
|
|
password: 'test',
|
|
|
|
|
}, params);
|
|
|
|
|
|
|
|
|
|
const res = await api('signup', q);
|
|
|
|
|
|
|
|
|
|
return res.body;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
|
2023-03-07 15:56:09 -08:00
|
|
|
|
const q = params;
|
2023-03-02 18:13:12 -08:00
|
|
|
|
|
|
|
|
|
const res = await api('notes/create', q, user);
|
|
|
|
|
|
|
|
|
|
return res.body ? res.body.createdNote : null;
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-19 04:26:38 -07:00
|
|
|
|
// 非公開ノートをAPI越しに見たときのノート NoteEntityService.ts
|
|
|
|
|
export const hiddenNote = (note: any): any => {
|
|
|
|
|
const temp = {
|
|
|
|
|
...note,
|
|
|
|
|
fileIds: [],
|
|
|
|
|
files: [],
|
|
|
|
|
text: null,
|
|
|
|
|
cw: null,
|
|
|
|
|
isHidden: true,
|
|
|
|
|
};
|
|
|
|
|
delete temp.visibleUserIds;
|
|
|
|
|
delete temp.poll;
|
|
|
|
|
return temp;
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-02 18:13:12 -08:00
|
|
|
|
export const react = async (user: any, note: any, reaction: string): Promise<any> => {
|
|
|
|
|
await api('notes/reactions/create', {
|
|
|
|
|
noteId: note.id,
|
|
|
|
|
reaction: reaction,
|
|
|
|
|
}, user);
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-17 17:01:10 -07:00
|
|
|
|
export const page = async (user: any, page: any = {}): Promise<any> => {
|
|
|
|
|
const res = await api('pages/create', {
|
|
|
|
|
alignCenter: false,
|
|
|
|
|
content: [
|
|
|
|
|
{
|
|
|
|
|
id: '2be9a64b-5ada-43a3-85f3-ec3429551ded',
|
|
|
|
|
text: 'Hello World!',
|
|
|
|
|
type: 'text',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
eyeCatchingImageId: null,
|
|
|
|
|
font: 'sans-serif',
|
|
|
|
|
hideTitleWhenPinned: false,
|
|
|
|
|
name: '1678594845072',
|
|
|
|
|
script: '',
|
|
|
|
|
summary: null,
|
|
|
|
|
title: '',
|
|
|
|
|
variables: [],
|
|
|
|
|
...page,
|
|
|
|
|
}, user);
|
|
|
|
|
return res.body;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const play = async (user: any, play: any = {}): Promise<any> => {
|
|
|
|
|
const res = await api('flash/create', {
|
|
|
|
|
permissions: [],
|
|
|
|
|
script: 'test',
|
|
|
|
|
summary: '',
|
|
|
|
|
title: 'test',
|
|
|
|
|
...play,
|
|
|
|
|
}, user);
|
|
|
|
|
return res.body;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const clip = async (user: any, clip: any = {}): Promise<any> => {
|
|
|
|
|
const res = await api('clips/create', {
|
|
|
|
|
description: null,
|
|
|
|
|
isPublic: true,
|
|
|
|
|
name: 'test',
|
|
|
|
|
...clip,
|
|
|
|
|
}, user);
|
|
|
|
|
return res.body;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const galleryPost = async (user: any, channel: any = {}): Promise<any> => {
|
|
|
|
|
const res = await api('gallery/posts/create', {
|
|
|
|
|
description: null,
|
|
|
|
|
fileIds: [],
|
|
|
|
|
isSensitive: false,
|
|
|
|
|
title: 'test',
|
|
|
|
|
...channel,
|
|
|
|
|
}, user);
|
|
|
|
|
return res.body;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const channel = async (user: any, channel: any = {}): Promise<any> => {
|
|
|
|
|
const res = await api('channels/create', {
|
|
|
|
|
bannerId: null,
|
|
|
|
|
description: null,
|
|
|
|
|
name: 'test',
|
|
|
|
|
...channel,
|
|
|
|
|
}, user);
|
|
|
|
|
return res.body;
|
|
|
|
|
};
|
|
|
|
|
|
2023-04-11 21:20:16 -07:00
|
|
|
|
export const role = async (user: any, role: any = {}, policies: any = {}): Promise<any> => {
|
|
|
|
|
const res = await api('admin/roles/create', {
|
|
|
|
|
asBadge: false,
|
|
|
|
|
canEditMembersByModerator: false,
|
|
|
|
|
color: null,
|
|
|
|
|
condFormula: {
|
|
|
|
|
id: 'ebef1684-672d-49b6-ad82-1b3ec3784f85',
|
|
|
|
|
type: 'isRemote',
|
|
|
|
|
},
|
|
|
|
|
description: '',
|
|
|
|
|
displayOrder: 0,
|
|
|
|
|
iconUrl: null,
|
|
|
|
|
isAdministrator: false,
|
|
|
|
|
isModerator: false,
|
|
|
|
|
isPublic: false,
|
|
|
|
|
name: 'New Role',
|
|
|
|
|
target: 'manual',
|
|
|
|
|
policies: {
|
|
|
|
|
...Object.entries(DEFAULT_POLICIES).map(([k, v]) => [k, {
|
|
|
|
|
priority: 0,
|
|
|
|
|
useDefault: true,
|
|
|
|
|
value: v,
|
|
|
|
|
}]),
|
|
|
|
|
...policies,
|
|
|
|
|
},
|
|
|
|
|
...role,
|
|
|
|
|
}, user);
|
|
|
|
|
return res.body;
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-02 18:13:12 -08:00
|
|
|
|
interface UploadOptions {
|
|
|
|
|
/** Optional, absolute path or relative from ./resources/ */
|
|
|
|
|
path?: string | URL;
|
|
|
|
|
/** The name to be used for the file upload */
|
|
|
|
|
name?: string;
|
|
|
|
|
/** A Blob can be provided instead of path */
|
|
|
|
|
blob?: Blob;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Upload file
|
|
|
|
|
* @param user User
|
|
|
|
|
*/
|
|
|
|
|
export const uploadFile = async (user: any, { path, name, blob }: UploadOptions = {}): Promise<any> => {
|
|
|
|
|
const absPath = path == null
|
|
|
|
|
? new URL('resources/Lenna.jpg', import.meta.url)
|
|
|
|
|
: isAbsolute(path.toString())
|
|
|
|
|
? new URL(path)
|
|
|
|
|
: 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');
|
|
|
|
|
if (name) {
|
|
|
|
|
formData.append('name', name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const res = await relativeFetch('api/drive/files/create', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: formData,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const body = res.status !== 204 ? await res.json() : null;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
status: res.status,
|
|
|
|
|
body,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const uploadUrl = async (user: any, url: string) => {
|
|
|
|
|
let file: any;
|
|
|
|
|
const marker = Math.random().toString();
|
|
|
|
|
|
|
|
|
|
const ws = await connectStream(user, 'main', (msg) => {
|
|
|
|
|
if (msg.type === 'urlUploadFinished' && msg.body.marker === marker) {
|
|
|
|
|
file = msg.body.file;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await api('drive/files/upload-from-url', {
|
|
|
|
|
url,
|
|
|
|
|
marker,
|
|
|
|
|
force: true,
|
|
|
|
|
}, user);
|
|
|
|
|
|
|
|
|
|
await sleep(7000);
|
|
|
|
|
ws.close();
|
|
|
|
|
|
|
|
|
|
return file;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function connectStream(user: any, 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}`);
|
|
|
|
|
|
|
|
|
|
ws.on('open', () => {
|
|
|
|
|
ws.on('message', data => {
|
|
|
|
|
const msg = JSON.parse(data.toString());
|
|
|
|
|
if (msg.type === 'channel' && msg.body.id === 'a') {
|
|
|
|
|
listener(msg.body);
|
|
|
|
|
} else if (msg.type === 'connected' && msg.body.id === 'a') {
|
|
|
|
|
res(ws);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ws.send(JSON.stringify({
|
|
|
|
|
type: 'connect',
|
|
|
|
|
body: {
|
|
|
|
|
channel: channel,
|
|
|
|
|
id: 'a',
|
|
|
|
|
pong: true,
|
|
|
|
|
params: params,
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const waitFire = async (user: any, 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;
|
|
|
|
|
|
|
|
|
|
let ws: WebSocket;
|
|
|
|
|
try {
|
|
|
|
|
ws = await connectStream(user, channel, msg => {
|
|
|
|
|
if (cond(msg)) {
|
|
|
|
|
ws.close();
|
|
|
|
|
if (timer) clearTimeout(timer);
|
|
|
|
|
res(true);
|
|
|
|
|
}
|
|
|
|
|
}, params);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
rej(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!ws!) return;
|
|
|
|
|
|
|
|
|
|
timer = setTimeout(() => {
|
|
|
|
|
ws.close();
|
|
|
|
|
res(false);
|
2023-04-07 04:05:15 -07:00
|
|
|
|
}, 3000);
|
2023-03-02 18:13:12 -08:00
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await trgr();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
ws.close();
|
|
|
|
|
if (timer) clearTimeout(timer);
|
|
|
|
|
rej(e);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-17 17:01:10 -07:00
|
|
|
|
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<SimpleGetResponse> => {
|
2023-03-02 18:13:12 -08:00
|
|
|
|
const res = await relativeFetch(path, {
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: accept,
|
2023-03-17 17:01:10 -07:00
|
|
|
|
Cookie: cookie,
|
2023-03-02 18:13:12 -08:00
|
|
|
|
},
|
|
|
|
|
redirect: 'manual',
|
|
|
|
|
});
|
|
|
|
|
|
2023-03-09 16:37:22 -08:00
|
|
|
|
const jsonTypes = [
|
|
|
|
|
'application/json; charset=utf-8',
|
|
|
|
|
'application/activity+json; charset=utf-8',
|
|
|
|
|
];
|
2023-03-17 17:01:10 -07:00
|
|
|
|
const htmlTypes = [
|
|
|
|
|
'text/html; charset=utf-8',
|
|
|
|
|
];
|
2023-03-09 16:37:22 -08:00
|
|
|
|
|
2023-03-17 17:01:10 -07:00
|
|
|
|
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;
|
2023-03-02 18:13:12 -08:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
status: res.status,
|
|
|
|
|
body,
|
|
|
|
|
type: res.headers.get('content-type'),
|
|
|
|
|
location: res.headers.get('location'),
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
|
|
|
|
|
|
|
|
|
|
const db = new DataSource({
|
|
|
|
|
type: 'postgres',
|
|
|
|
|
host: config.db.host,
|
|
|
|
|
port: config.db.port,
|
|
|
|
|
username: config.db.user,
|
|
|
|
|
password: config.db.pass,
|
|
|
|
|
database: config.db.db,
|
|
|
|
|
synchronize: true && !justBorrow,
|
|
|
|
|
dropSchema: true && !justBorrow,
|
|
|
|
|
entities: initEntities ?? entities,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.initialize();
|
|
|
|
|
|
|
|
|
|
return db;
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-26 03:16:32 -07:00
|
|
|
|
export function sleep(msec: number) {
|
|
|
|
|
return new Promise<void>(res => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
res();
|
|
|
|
|
}, msec);
|
|
|
|
|
});
|
|
|
|
|
}
|