summaryrefslogtreecommitdiff
path: root/packages/misskey-js/src/api.ts
blob: ed1282957fbe3e613c767e0bc078bbb8138a9407 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import './autogen/apiClientJSDoc.js';

import { endpointReqTypes } from './autogen/endpoint.js';
import type { SwitchCaseResponseType, Endpoints } from './api.types.js';

export type {
	SwitchCaseResponseType,
} from './api.types.js';

const MK_API_ERROR = Symbol();

export type APIError = {
	id: string;
	code: string;
	message: string;
	kind: 'client' | 'server';
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	info: Record<string, any>;
};

export function isAPIError(reason: Record<PropertyKey, unknown>): reason is APIError {
	return reason[MK_API_ERROR] === true;
}

export type FetchLike = (input: string, init?: {
	method?: string;
	body?: Blob | FormData | string;
	credentials?: RequestCredentials;
	cache?: RequestCache;
	headers: { [key in string]: string }
}) => Promise<{
	status: number;
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	json(): Promise<any>;
}>;

export class APIClient {
	public origin: string;
	public credential: string | null | undefined;
	public fetch: FetchLike;

	constructor(opts: {
		origin: APIClient['origin'];
		credential?: APIClient['credential'];
		fetch?: APIClient['fetch'] | null | undefined;
	}) {
		this.origin = opts.origin;
		this.credential = opts.credential;
		// ネイティブ関数をそのまま変数に代入して使おうとするとChromiumではIllegal invocationエラーが発生するため、
		// 環境で実装されているfetchを使う場合は無名関数でラップして使用する
		this.fetch = opts.fetch ?? ((...args) => fetch(...args));
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	private assertIsRecord<T>(obj: T): obj is T & Record<string, any> {
		return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
	}

	private assertSpecialEpReqType(ep: keyof Endpoints): ep is keyof typeof endpointReqTypes {
		return ep in endpointReqTypes;
	}

	public request<E extends keyof Endpoints, P extends Endpoints[E]['req']>(
		endpoint: E,
		params: P = {} as P,
		credential?: string | null,
	): Promise<SwitchCaseResponseType<E, P>> {
		return new Promise((resolve, reject) => {
			let mediaType = 'application/json';
			// (autogenがバグったときのため、念の為nullチェックも行う)
			// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
			if (this.assertSpecialEpReqType(endpoint) && endpointReqTypes[endpoint] != null) {
				mediaType = endpointReqTypes[endpoint];
			}

			let payload: FormData | string = '{}';

			if (mediaType === 'application/json') {
				payload = JSON.stringify({
					...params,
					i: credential !== undefined ? credential : this.credential,
				});
			} else if (mediaType === 'multipart/form-data') {
				payload = new FormData();
				const i = credential !== undefined ? credential : this.credential;
				if (i != null) {
					payload.append('i', i);
				}
				if (this.assertIsRecord(params)) {
					for (const key in params) {
						const value = params[key];

						if (value == null) continue;

						if (value instanceof File || value instanceof Blob) {
							payload.append(key, value);
						} else if (typeof value === 'object') {
							payload.append(key, JSON.stringify(value));
						} else {
							payload.append(key, value);
						}
					}
				}
			}

			this.fetch(`${this.origin}/api/${endpoint}`, {
				method: 'POST',
				body: payload,
				headers: {
					'Content-Type': mediaType,
				},
				credentials: 'omit',
				cache: 'no-cache',
			}).then(async (res) => {
				const body = res.status === 204 ? null : await res.json();

				if (res.status === 200 || res.status === 204) {
					resolve(body);
				} else {
					reject({
						[MK_API_ERROR]: true,
						...body.error,
					});
				}
			}).catch(reject);
		});
	}
}