summaryrefslogtreecommitdiff
path: root/packages/misskey-js/src
diff options
context:
space:
mode:
authorKagami Sascha Rosylight <saschanaz@outlook.com>2023-03-25 08:09:24 +0100
committerKagami Sascha Rosylight <saschanaz@outlook.com>2023-03-25 08:09:24 +0100
commit3b524f32bfadfa76d28ef26600642bd190118da3 (patch)
tree9c4d37a3eeb902b65f0d8e487106282a11578ec7 /packages/misskey-js/src
parentUpdate CHANGELOG.md (diff)
parentfeat: add type of gallery (#55) (diff)
downloadmisskey-3b524f32bfadfa76d28ef26600642bd190118da3.tar.gz
misskey-3b524f32bfadfa76d28ef26600642bd190118da3.tar.bz2
misskey-3b524f32bfadfa76d28ef26600642bd190118da3.zip
Subtree merged in packages/misskey-js
Diffstat (limited to 'packages/misskey-js/src')
-rw-r--r--packages/misskey-js/src/acct.ts14
-rw-r--r--packages/misskey-js/src/api.ts102
-rw-r--r--packages/misskey-js/src/api.types.ts605
-rw-r--r--packages/misskey-js/src/consts.ts42
-rw-r--r--packages/misskey-js/src/entities.ts508
-rw-r--r--packages/misskey-js/src/index.ts26
-rw-r--r--packages/misskey-js/src/streaming.ts340
-rw-r--r--packages/misskey-js/src/streaming.types.ts152
8 files changed, 1789 insertions, 0 deletions
diff --git a/packages/misskey-js/src/acct.ts b/packages/misskey-js/src/acct.ts
new file mode 100644
index 0000000000..c32cee86c9
--- /dev/null
+++ b/packages/misskey-js/src/acct.ts
@@ -0,0 +1,14 @@
+export type Acct = {
+ username: string;
+ host: string | null;
+};
+
+export function parse(acct: string): Acct {
+ if (acct.startsWith('@')) acct = acct.substr(1);
+ const split = acct.split('@', 2);
+ return { username: split[0], host: split[1] || null };
+}
+
+export function toString(acct: Acct): string {
+ return acct.host == null ? acct.username : `${acct.username}@${acct.host}`;
+}
diff --git a/packages/misskey-js/src/api.ts b/packages/misskey-js/src/api.ts
new file mode 100644
index 0000000000..1704b6e462
--- /dev/null
+++ b/packages/misskey-js/src/api.ts
@@ -0,0 +1,102 @@
+import { Endpoints } from './api.types';
+
+const MK_API_ERROR = Symbol();
+
+export type APIError = {
+ id: string;
+ code: string;
+ message: string;
+ kind: 'client' | 'server';
+ info: Record<string, any>;
+};
+
+export function isAPIError(reason: any): reason is APIError {
+ return reason[MK_API_ERROR] === true;
+}
+
+export type FetchLike = (input: string, init?: {
+ method?: string;
+ body?: string;
+ credentials?: RequestCredentials;
+ cache?: RequestCache;
+ headers: {[key in string]: string}
+ }) => Promise<{
+ status: number;
+ json(): Promise<any>;
+ }>;
+
+type IsNeverType<T> = [T] extends [never] ? true : false;
+
+type StrictExtract<Union, Cond> = Cond extends Union ? Union : never;
+
+type IsCaseMatched<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> =
+ IsNeverType<StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>> extends false ? true : false;
+
+type GetCaseResult<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> =
+ StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>[1];
+
+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));
+ }
+
+ public request<E extends keyof Endpoints, P extends Endpoints[E]['req']>(
+ endpoint: E, params: P = {} as P, credential?: string | null | undefined,
+ ): Promise<Endpoints[E]['res'] extends { $switch: { $cases: [any, any][]; $default: any; }; }
+ ?
+ IsCaseMatched<E, P, 0> extends true ? GetCaseResult<E, P, 0> :
+ IsCaseMatched<E, P, 1> extends true ? GetCaseResult<E, P, 1> :
+ IsCaseMatched<E, P, 2> extends true ? GetCaseResult<E, P, 2> :
+ IsCaseMatched<E, P, 3> extends true ? GetCaseResult<E, P, 3> :
+ IsCaseMatched<E, P, 4> extends true ? GetCaseResult<E, P, 4> :
+ IsCaseMatched<E, P, 5> extends true ? GetCaseResult<E, P, 5> :
+ IsCaseMatched<E, P, 6> extends true ? GetCaseResult<E, P, 6> :
+ IsCaseMatched<E, P, 7> extends true ? GetCaseResult<E, P, 7> :
+ IsCaseMatched<E, P, 8> extends true ? GetCaseResult<E, P, 8> :
+ IsCaseMatched<E, P, 9> extends true ? GetCaseResult<E, P, 9> :
+ Endpoints[E]['res']['$switch']['$default']
+ : Endpoints[E]['res']>
+ {
+ const promise = new Promise((resolve, reject) => {
+ this.fetch(`${this.origin}/api/${endpoint}`, {
+ method: 'POST',
+ body: JSON.stringify({
+ ...params,
+ i: credential !== undefined ? credential : this.credential,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'omit',
+ cache: 'no-cache',
+ }).then(async (res) => {
+ const body = res.status === 204 ? null : await res.json();
+
+ if (res.status === 200) {
+ resolve(body);
+ } else if (res.status === 204) {
+ resolve(null);
+ } else {
+ reject({
+ [MK_API_ERROR]: true,
+ ...body.error,
+ });
+ }
+ }).catch(reject);
+ });
+
+ return promise as any;
+ }
+}
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
new file mode 100644
index 0000000000..4fbf42f917
--- /dev/null
+++ b/packages/misskey-js/src/api.types.ts
@@ -0,0 +1,605 @@
+import {
+ Ad, Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, DetailedInstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, GalleryPost, Instance, InstanceMetadata,
+ LiteInstanceMetadata,
+ MeDetailed,
+ Note, NoteFavorite, OriginType, Page, ServerInfo, Stats, User, UserDetailed, UserGroup, UserList, UserSorting, Notification, NoteReaction, Signin, MessagingMessage,
+} from './entities';
+
+type TODO = Record<string, any> | null;
+
+type NoParams = Record<string, never>;
+
+type ShowUserReq = { username: string; host?: string; } | { userId: User['id']; };
+
+export type Endpoints = {
+ // admin
+ 'admin/abuse-user-reports': { req: TODO; res: TODO; };
+ 'admin/delete-all-files-of-a-user': { req: { userId: User['id']; }; res: null; };
+ 'admin/delete-logs': { req: NoParams; res: null; };
+ 'admin/get-index-stats': { req: TODO; res: TODO; };
+ 'admin/get-table-stats': { req: TODO; res: TODO; };
+ 'admin/invite': { req: TODO; res: TODO; };
+ 'admin/logs': { req: TODO; res: TODO; };
+ 'admin/reset-password': { req: TODO; res: TODO; };
+ 'admin/resolve-abuse-user-report': { req: TODO; res: TODO; };
+ 'admin/resync-chart': { req: TODO; res: TODO; };
+ 'admin/send-email': { req: TODO; res: TODO; };
+ 'admin/server-info': { req: TODO; res: TODO; };
+ 'admin/show-moderation-logs': { req: TODO; res: TODO; };
+ 'admin/show-user': { req: TODO; res: TODO; };
+ 'admin/show-users': { req: TODO; res: TODO; };
+ 'admin/silence-user': { req: TODO; res: TODO; };
+ 'admin/suspend-user': { req: TODO; res: TODO; };
+ 'admin/unsilence-user': { req: TODO; res: TODO; };
+ 'admin/unsuspend-user': { req: TODO; res: TODO; };
+ 'admin/update-meta': { req: TODO; res: TODO; };
+ 'admin/vacuum': { req: TODO; res: TODO; };
+ 'admin/accounts/create': { req: TODO; res: TODO; };
+ 'admin/ad/create': { req: TODO; res: TODO; };
+ 'admin/ad/delete': { req: { id: Ad['id']; }; res: null; };
+ 'admin/ad/list': { req: TODO; res: TODO; };
+ 'admin/ad/update': { req: TODO; res: TODO; };
+ 'admin/announcements/create': { req: TODO; res: TODO; };
+ 'admin/announcements/delete': { req: { id: Announcement['id'] }; res: null; };
+ 'admin/announcements/list': { req: TODO; res: TODO; };
+ 'admin/announcements/update': { req: TODO; res: TODO; };
+ 'admin/drive/clean-remote-files': { req: TODO; res: TODO; };
+ 'admin/drive/cleanup': { req: TODO; res: TODO; };
+ 'admin/drive/files': { req: TODO; res: TODO; };
+ 'admin/drive/show-file': { req: TODO; res: TODO; };
+ 'admin/emoji/add': { req: TODO; res: TODO; };
+ 'admin/emoji/copy': { req: TODO; res: TODO; };
+ 'admin/emoji/list-remote': { req: TODO; res: TODO; };
+ 'admin/emoji/list': { req: TODO; res: TODO; };
+ 'admin/emoji/remove': { req: TODO; res: TODO; };
+ 'admin/emoji/update': { req: TODO; res: TODO; };
+ 'admin/federation/delete-all-files': { req: { host: string; }; res: null; };
+ 'admin/federation/refresh-remote-instance-metadata': { req: TODO; res: TODO; };
+ 'admin/federation/remove-all-following': { req: TODO; res: TODO; };
+ 'admin/federation/update-instance': { req: TODO; res: TODO; };
+ 'admin/moderators/add': { req: TODO; res: TODO; };
+ 'admin/moderators/remove': { req: TODO; res: TODO; };
+ 'admin/promo/create': { req: TODO; res: TODO; };
+ 'admin/queue/clear': { req: TODO; res: TODO; };
+ 'admin/queue/deliver-delayed': { req: TODO; res: TODO; };
+ 'admin/queue/inbox-delayed': { req: TODO; res: TODO; };
+ 'admin/queue/jobs': { req: TODO; res: TODO; };
+ 'admin/queue/stats': { req: TODO; res: TODO; };
+ 'admin/relays/add': { req: TODO; res: TODO; };
+ 'admin/relays/list': { req: TODO; res: TODO; };
+ 'admin/relays/remove': { req: TODO; res: TODO; };
+
+ // announcements
+ 'announcements': { req: { limit?: number; withUnreads?: boolean; sinceId?: Announcement['id']; untilId?: Announcement['id']; }; res: Announcement[]; };
+
+ // antennas
+ 'antennas/create': { req: TODO; res: Antenna; };
+ 'antennas/delete': { req: { antennaId: Antenna['id']; }; res: null; };
+ 'antennas/list': { req: NoParams; res: Antenna[]; };
+ 'antennas/notes': { req: { antennaId: Antenna['id']; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; };
+ 'antennas/show': { req: { antennaId: Antenna['id']; }; res: Antenna; };
+ 'antennas/update': { req: TODO; res: Antenna; };
+
+ // ap
+ 'ap/get': { req: { uri: string; }; res: Record<string, any>; };
+ 'ap/show': { req: { uri: string; }; res: {
+ type: 'Note';
+ object: Note;
+ } | {
+ type: 'User';
+ object: UserDetailed;
+ }; };
+
+ // app
+ 'app/create': { req: TODO; res: App; };
+ 'app/show': { req: { appId: App['id']; }; res: App; };
+
+ // auth
+ 'auth/accept': { req: { token: string; }; res: null; };
+ 'auth/session/generate': { req: { appSecret: string; }; res: { token: string; url: string; }; };
+ 'auth/session/show': { req: { token: string; }; res: AuthSession; };
+ 'auth/session/userkey': { req: { appSecret: string; token: string; }; res: { accessToken: string; user: User }; };
+
+ // blocking
+ 'blocking/create': { req: { userId: User['id'] }; res: UserDetailed; };
+ 'blocking/delete': { req: { userId: User['id'] }; res: UserDetailed; };
+ 'blocking/list': { req: { limit?: number; sinceId?: Blocking['id']; untilId?: Blocking['id']; }; res: Blocking[]; };
+
+ // channels
+ 'channels/create': { req: TODO; res: TODO; };
+ 'channels/featured': { req: TODO; res: TODO; };
+ 'channels/follow': { req: TODO; res: TODO; };
+ 'channels/followed': { req: TODO; res: TODO; };
+ 'channels/owned': { req: TODO; res: TODO; };
+ 'channels/pin-note': { req: TODO; res: TODO; };
+ 'channels/show': { req: TODO; res: TODO; };
+ 'channels/timeline': { req: TODO; res: TODO; };
+ 'channels/unfollow': { req: TODO; res: TODO; };
+ 'channels/update': { req: TODO; res: TODO; };
+
+ // charts
+ 'charts/active-users': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: {
+ local: {
+ users: number[];
+ };
+ remote: {
+ users: number[];
+ };
+ }; };
+ 'charts/drive': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: {
+ local: {
+ decCount: number[];
+ decSize: number[];
+ incCount: number[];
+ incSize: number[];
+ totalCount: number[];
+ totalSize: number[];
+ };
+ remote: {
+ decCount: number[];
+ decSize: number[];
+ incCount: number[];
+ incSize: number[];
+ totalCount: number[];
+ totalSize: number[];
+ };
+ }; };
+ 'charts/federation': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: {
+ instance: {
+ dec: number[];
+ inc: number[];
+ total: number[];
+ };
+ }; };
+ 'charts/hashtag': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: TODO; };
+ 'charts/instance': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; host: string; }; res: {
+ drive: {
+ decFiles: number[];
+ decUsage: number[];
+ incFiles: number[];
+ incUsage: number[];
+ totalFiles: number[];
+ totalUsage: number[];
+ };
+ followers: {
+ dec: number[];
+ inc: number[];
+ total: number[];
+ };
+ following: {
+ dec: number[];
+ inc: number[];
+ total: number[];
+ };
+ notes: {
+ dec: number[];
+ inc: number[];
+ total: number[];
+ diffs: {
+ normal: number[];
+ renote: number[];
+ reply: number[];
+ };
+ };
+ requests: {
+ failed: number[];
+ received: number[];
+ succeeded: number[];
+ };
+ users: {
+ dec: number[];
+ inc: number[];
+ total: number[];
+ };
+ }; };
+ 'charts/network': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: TODO; };
+ 'charts/notes': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: {
+ local: {
+ dec: number[];
+ inc: number[];
+ total: number[];
+ diffs: {
+ normal: number[];
+ renote: number[];
+ reply: number[];
+ };
+ };
+ remote: {
+ dec: number[];
+ inc: number[];
+ total: number[];
+ diffs: {
+ normal: number[];
+ renote: number[];
+ reply: number[];
+ };
+ };
+ }; };
+ 'charts/user/drive': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; userId: User['id']; }; res: {
+ decCount: number[];
+ decSize: number[];
+ incCount: number[];
+ incSize: number[];
+ totalCount: number[];
+ totalSize: number[];
+ }; };
+ 'charts/user/following': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; userId: User['id']; }; res: TODO; };
+ 'charts/user/notes': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; userId: User['id']; }; res: {
+ dec: number[];
+ inc: number[];
+ total: number[];
+ diffs: {
+ normal: number[];
+ renote: number[];
+ reply: number[];
+ };
+ }; };
+ 'charts/user/reactions': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; userId: User['id']; }; res: TODO; };
+ 'charts/users': { req: { span: 'day' | 'hour'; limit?: number; offset?: number | null; }; res: {
+ local: {
+ dec: number[];
+ inc: number[];
+ total: number[];
+ };
+ remote: {
+ dec: number[];
+ inc: number[];
+ total: number[];
+ };
+ }; };
+
+ // clips
+ 'clips/add-note': { req: TODO; res: TODO; };
+ 'clips/create': { req: TODO; res: TODO; };
+ 'clips/delete': { req: { clipId: Clip['id']; }; res: null; };
+ 'clips/list': { req: TODO; res: TODO; };
+ 'clips/notes': { req: TODO; res: TODO; };
+ 'clips/show': { req: TODO; res: TODO; };
+ 'clips/update': { req: TODO; res: TODO; };
+
+ // drive
+ 'drive': { req: NoParams; res: { capacity: number; usage: number; }; };
+ '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/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[]; };
+ 'drive/files/show': { req: { fileId?: DriveFile['id']; url?: string; }; res: DriveFile; };
+ 'drive/files/update': { req: { fileId: DriveFile['id']; folderId?: DriveFolder['id'] | null; name?: string; isSensitive?: boolean; comment?: string | null; }; res: DriveFile; };
+ 'drive/files/upload-from-url': { req: { url: string; folderId?: DriveFolder['id'] | null; isSensitive?: boolean; comment?: string | null; marker?: string | null; force?: boolean; }; res: null; };
+ 'drive/folders': { req: { folderId?: DriveFolder['id'] | null; limit?: number; sinceId?: DriveFile['id']; untilId?: DriveFile['id']; }; res: DriveFolder[]; };
+ 'drive/folders/create': { req: { name?: string; parentId?: DriveFolder['id'] | null; }; res: DriveFolder; };
+ 'drive/folders/delete': { req: { folderId: DriveFolder['id']; }; res: null; };
+ 'drive/folders/find': { req: { name: string; parentId?: DriveFolder['id'] | null; }; res: DriveFolder[]; };
+ 'drive/folders/show': { req: { folderId: DriveFolder['id']; }; res: DriveFolder; };
+ 'drive/folders/update': { req: { folderId: DriveFolder['id']; name?: string; parentId?: DriveFolder['id'] | null; }; res: DriveFolder; };
+ 'drive/stream': { req: { type?: DriveFile['type'] | null; limit?: number; sinceId?: DriveFile['id']; untilId?: DriveFile['id']; }; res: DriveFile[]; };
+
+ // endpoint
+ 'endpoint': { req: { endpoint: string; }; res: { params: { name: string; type: string; }[]; }; };
+
+ // endpoints
+ 'endpoints': { req: NoParams; res: string[]; };
+
+ // federation
+ 'federation/dns': { req: { host: string; }; res: {
+ a: string[];
+ aaaa: string[];
+ cname: string[];
+ txt: string[];
+ }; };
+ 'federation/followers': { req: { host: string; limit?: number; sinceId?: Following['id']; untilId?: Following['id']; }; res: FollowingFolloweePopulated[]; };
+ 'federation/following': { req: { host: string; limit?: number; sinceId?: Following['id']; untilId?: Following['id']; }; res: FollowingFolloweePopulated[]; };
+ 'federation/instances': { req: {
+ host?: string | null;
+ blocked?: boolean | null;
+ notResponding?: boolean | null;
+ suspended?: boolean | null;
+ federating?: boolean | null;
+ subscribing?: boolean | null;
+ publishing?: boolean | null;
+ limit?: number;
+ offset?: number;
+ sort?: '+pubSub' | '-pubSub' | '+notes' | '-notes' | '+users' | '-users' | '+following' | '-following' | '+followers' | '-followers' | '+caughtAt' | '-caughtAt' | '+lastCommunicatedAt' | '-lastCommunicatedAt' | '+driveUsage' | '-driveUsage' | '+driveFiles' | '-driveFiles';
+ }; res: Instance[]; };
+ 'federation/show-instance': { req: { host: string; }; res: Instance; };
+ 'federation/update-remote-user': { req: { userId: User['id']; }; res: null; };
+ 'federation/users': { req: { host: string; limit?: number; sinceId?: User['id']; untilId?: User['id']; }; res: UserDetailed[]; };
+
+ // following
+ 'following/create': { req: { userId: User['id'] }; res: User; };
+ 'following/delete': { req: { userId: User['id'] }; res: User; };
+ 'following/requests/accept': { req: { userId: User['id'] }; res: null; };
+ 'following/requests/cancel': { req: { userId: User['id'] }; res: User; };
+ 'following/requests/list': { req: NoParams; res: FollowRequest[]; };
+ 'following/requests/reject': { req: { userId: User['id'] }; res: null; };
+
+ // gallery
+ 'gallery/featured': { req: null; res: GalleryPost[]; };
+ 'gallery/popular': { req: null; res: GalleryPost[]; };
+ 'gallery/posts': { req: { limit?: number; sinceId?: GalleryPost['id']; untilId?: GalleryPost['id']; }; res: GalleryPost[]; };
+ 'gallery/posts/create': { req: { title: GalleryPost['title']; description?: GalleryPost['description']; fileIds: GalleryPost['fileIds']; isSensitive?: GalleryPost['isSensitive'] }; res: GalleryPost; };
+ 'gallery/posts/delete': { req: { postId: GalleryPost['id'] }; res: null; };
+ 'gallery/posts/like': { req: { postId: GalleryPost['id'] }; res: null; };
+ 'gallery/posts/show': { req: { postId: GalleryPost['id'] }; res: GalleryPost; };
+ 'gallery/posts/unlike': { req: { postId: GalleryPost['id'] }; res: null; };
+ 'gallery/posts/update': { req: { postId: GalleryPost['id']; title: GalleryPost['title']; description?: GalleryPost['description']; fileIds: GalleryPost['fileIds']; isSensitive?: GalleryPost['isSensitive'] }; res: GalleryPost; };
+
+ // games
+ 'games/reversi/games': { req: TODO; res: TODO; };
+ 'games/reversi/games/show': { req: TODO; res: TODO; };
+ 'games/reversi/games/surrender': { req: TODO; res: TODO; };
+ 'games/reversi/invitations': { req: TODO; res: TODO; };
+ 'games/reversi/match': { req: TODO; res: TODO; };
+ 'games/reversi/match/cancel': { req: TODO; res: TODO; };
+
+ // get-online-users-count
+ 'get-online-users-count': { req: NoParams; res: { count: number; }; };
+
+ // hashtags
+ 'hashtags/list': { req: TODO; res: TODO; };
+ 'hashtags/search': { req: TODO; res: TODO; };
+ 'hashtags/show': { req: TODO; res: TODO; };
+ 'hashtags/trend': { req: TODO; res: TODO; };
+ 'hashtags/users': { req: TODO; res: TODO; };
+
+ // i
+ 'i': { req: NoParams; res: User; };
+ 'i/apps': { req: TODO; res: TODO; };
+ 'i/authorized-apps': { req: TODO; res: TODO; };
+ 'i/change-password': { req: TODO; res: TODO; };
+ 'i/delete-account': { req: { password: string; }; res: null; };
+ 'i/export-blocking': { req: TODO; res: TODO; };
+ 'i/export-following': { req: TODO; res: TODO; };
+ 'i/export-mute': { req: TODO; res: TODO; };
+ 'i/export-notes': { req: TODO; res: TODO; };
+ 'i/export-user-lists': { req: TODO; res: TODO; };
+ 'i/favorites': { req: { limit?: number; sinceId?: NoteFavorite['id']; untilId?: NoteFavorite['id']; }; res: NoteFavorite[]; };
+ 'i/gallery/likes': { req: TODO; res: TODO; };
+ 'i/gallery/posts': { req: TODO; res: TODO; };
+ 'i/get-word-muted-notes-count': { req: TODO; res: TODO; };
+ 'i/import-following': { req: TODO; res: TODO; };
+ 'i/import-user-lists': { req: TODO; res: TODO; };
+ 'i/notifications': { req: {
+ limit?: number;
+ sinceId?: Notification['id'];
+ untilId?: Notification['id'];
+ following?: boolean;
+ markAsRead?: boolean;
+ includeTypes?: Notification['type'][];
+ excludeTypes?: Notification['type'][];
+ }; res: Notification[]; };
+ 'i/page-likes': { req: TODO; res: TODO; };
+ 'i/pages': { req: TODO; res: TODO; };
+ 'i/pin': { req: { noteId: Note['id']; }; res: MeDetailed; };
+ 'i/read-all-messaging-messages': { req: TODO; res: TODO; };
+ 'i/read-all-unread-notes': { req: TODO; res: TODO; };
+ 'i/read-announcement': { req: TODO; res: TODO; };
+ 'i/regenerate-token': { req: { password: string; }; res: null; };
+ 'i/registry/get-all': { req: { scope?: string[]; }; res: Record<string, any>; };
+ 'i/registry/get-detail': { req: { key: string; scope?: string[]; }; res: { updatedAt: DateString; value: any; }; };
+ 'i/registry/get': { req: { key: string; scope?: string[]; }; res: any; };
+ 'i/registry/keys-with-type': { req: { scope?: string[]; }; res: Record<string, 'null' | 'array' | 'number' | 'string' | 'boolean' | 'object'>; };
+ 'i/registry/keys': { req: { scope?: string[]; }; res: string[]; };
+ 'i/registry/remove': { req: { key: string; scope?: string[]; }; res: null; };
+ 'i/registry/scopes': { req: NoParams; res: string[][]; };
+ 'i/registry/set': { req: { key: string; value: any; scope?: string[]; }; res: null; };
+ 'i/revoke-token': { req: TODO; res: TODO; };
+ 'i/signin-history': { req: { limit?: number; sinceId?: Signin['id']; untilId?: Signin['id']; }; res: Signin[]; };
+ 'i/unpin': { req: { noteId: Note['id']; }; res: MeDetailed; };
+ 'i/update-email': { req: {
+ password: string;
+ email?: string | null;
+ }; res: MeDetailed; };
+ 'i/update': { req: {
+ name?: string | null;
+ description?: string | null;
+ lang?: string | null;
+ location?: string | null;
+ birthday?: string | null;
+ avatarId?: DriveFile['id'] | null;
+ bannerId?: DriveFile['id'] | null;
+ fields?: {
+ name: string;
+ value: string;
+ }[];
+ isLocked?: boolean;
+ isExplorable?: boolean;
+ hideOnlineStatus?: boolean;
+ carefulBot?: boolean;
+ autoAcceptFollowed?: boolean;
+ noCrawle?: boolean;
+ isBot?: boolean;
+ isCat?: boolean;
+ injectFeaturedNote?: boolean;
+ receiveAnnouncementEmail?: boolean;
+ alwaysMarkNsfw?: boolean;
+ mutedWords?: string[][];
+ mutingNotificationTypes?: Notification['type'][];
+ emailNotificationTypes?: string[];
+ }; res: MeDetailed; };
+ 'i/user-group-invites': { req: TODO; res: TODO; };
+ 'i/2fa/done': { req: TODO; res: TODO; };
+ 'i/2fa/key-done': { req: TODO; res: TODO; };
+ 'i/2fa/password-less': { req: TODO; res: TODO; };
+ 'i/2fa/register-key': { req: TODO; res: TODO; };
+ 'i/2fa/register': { req: TODO; res: TODO; };
+ 'i/2fa/remove-key': { req: TODO; res: TODO; };
+ 'i/2fa/unregister': { req: TODO; res: TODO; };
+
+ // messaging
+ 'messaging/history': { req: { limit?: number; group?: boolean; }; res: MessagingMessage[]; };
+ 'messaging/messages': { req: { userId?: User['id']; groupId?: UserGroup['id']; limit?: number; sinceId?: MessagingMessage['id']; untilId?: MessagingMessage['id']; markAsRead?: boolean; }; res: MessagingMessage[]; };
+ 'messaging/messages/create': { req: { userId?: User['id']; groupId?: UserGroup['id']; text?: string; fileId?: DriveFile['id']; }; res: MessagingMessage; };
+ 'messaging/messages/delete': { req: { messageId: MessagingMessage['id']; }; res: null; };
+ 'messaging/messages/read': { req: { messageId: MessagingMessage['id']; }; res: null; };
+
+ // meta
+ 'meta': { req: { detail?: boolean; }; res: {
+ $switch: {
+ $cases: [[
+ { detail: true; },
+ DetailedInstanceMetadata,
+ ], [
+ { detail: false; },
+ LiteInstanceMetadata,
+ ], [
+ { detail: boolean; },
+ LiteInstanceMetadata | DetailedInstanceMetadata,
+ ]];
+ $default: LiteInstanceMetadata;
+ };
+ }; };
+
+ // miauth
+ 'miauth/gen-token': { req: TODO; res: TODO; };
+
+ // mute
+ 'mute/create': { req: TODO; res: TODO; };
+ 'mute/delete': { req: { userId: User['id'] }; res: null; };
+ 'mute/list': { req: TODO; res: TODO; };
+
+ // my
+ 'my/apps': { req: TODO; res: TODO; };
+
+ // notes
+ 'notes': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; };
+ 'notes/children': { req: { noteId: Note['id']; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; };
+ 'notes/clips': { req: TODO; res: TODO; };
+ 'notes/conversation': { req: TODO; res: TODO; };
+ 'notes/create': { req: {
+ visibility?: 'public' | 'home' | 'followers' | 'specified',
+ visibleUserIds?: User['id'][];
+ text?: null | string;
+ cw?: null | string;
+ viaMobile?: boolean;
+ localOnly?: boolean;
+ fileIds?: DriveFile['id'][];
+ replyId?: null | Note['id'];
+ renoteId?: null | Note['id'];
+ channelId?: null | Channel['id'];
+ poll?: null | {
+ choices: string[];
+ multiple?: boolean;
+ expiresAt?: null | number;
+ expiredAfter?: null | number;
+ };
+ }; res: { createdNote: Note }; };
+ 'notes/delete': { req: { noteId: Note['id']; }; res: null; };
+ 'notes/favorites/create': { req: { noteId: Note['id']; }; res: null; };
+ 'notes/favorites/delete': { req: { noteId: Note['id']; }; res: null; };
+ 'notes/featured': { req: TODO; res: Note[]; };
+ 'notes/global-timeline': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; };
+ 'notes/hybrid-timeline': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; };
+ 'notes/local-timeline': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; };
+ 'notes/mentions': { req: { following?: boolean; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; };
+ 'notes/polls/recommendation': { req: TODO; res: TODO; };
+ 'notes/polls/vote': { req: { noteId: Note['id']; choice: number; }; res: null; };
+ 'notes/reactions': { req: { noteId: Note['id']; type?: string | null; limit?: number; }; res: NoteReaction[]; };
+ 'notes/reactions/create': { req: { noteId: Note['id']; reaction: string; }; res: null; };
+ 'notes/reactions/delete': { req: { noteId: Note['id']; }; res: null; };
+ 'notes/renotes': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; noteId: Note['id']; }; res: Note[]; };
+ 'notes/replies': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; noteId: Note['id']; }; res: Note[]; };
+ 'notes/search-by-tag': { req: TODO; res: TODO; };
+ 'notes/search': { req: TODO; res: TODO; };
+ 'notes/show': { req: { noteId: Note['id']; }; res: Note; };
+ 'notes/state': { req: TODO; res: TODO; };
+ 'notes/timeline': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; };
+ 'notes/unrenote': { req: { noteId: Note['id']; }; res: null; };
+ 'notes/user-list-timeline': { req: { listId: UserList['id']; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; };
+ 'notes/watching/create': { req: TODO; res: TODO; };
+ 'notes/watching/delete': { req: { noteId: Note['id']; }; res: null; };
+
+ // notifications
+ 'notifications/create': { req: { body: string; header?: string | null; icon?: string | null; }; res: null; };
+ 'notifications/mark-all-as-read': { req: NoParams; res: null; };
+ 'notifications/read': { req: { notificationId: Notification['id']; }; res: null; };
+
+ // page-push
+ 'page-push': { req: { pageId: Page['id']; event: string; var?: any; }; res: null; };
+
+ // pages
+ 'pages/create': { req: TODO; res: Page; };
+ 'pages/delete': { req: { pageId: Page['id']; }; res: null; };
+ 'pages/featured': { req: NoParams; res: Page[]; };
+ 'pages/like': { req: { pageId: Page['id']; }; res: null; };
+ 'pages/show': { req: { pageId?: Page['id']; name?: string; username?: string; }; res: Page; };
+ 'pages/unlike': { req: { pageId: Page['id']; }; res: null; };
+ 'pages/update': { req: TODO; res: null; };
+
+ // ping
+ 'ping': { req: NoParams; res: { pong: number; }; };
+
+ // pinned-users
+ 'pinned-users': { req: TODO; res: TODO; };
+
+ // promo
+ 'promo/read': { req: TODO; res: TODO; };
+
+ // request-reset-password
+ 'request-reset-password': { req: { username: string; email: string; }; res: null; };
+
+ // reset-password
+ 'reset-password': { req: { token: string; password: string; }; res: null; };
+
+ // room
+ 'room/show': { req: TODO; res: TODO; };
+ 'room/update': { req: TODO; res: TODO; };
+
+ // stats
+ 'stats': { req: NoParams; res: Stats; };
+
+ // server-info
+ 'server-info': { req: NoParams; res: ServerInfo; };
+
+ // sw
+ 'sw/register': { req: TODO; res: TODO; };
+
+ // username
+ 'username/available': { req: { username: string; }; res: { available: boolean; }; };
+
+ // users
+ 'users': { req: { limit?: number; offset?: number; sort?: UserSorting; origin?: OriginType; }; res: User[]; };
+ 'users/clips': { req: TODO; res: TODO; };
+ 'users/followers': { req: { userId?: User['id']; username?: User['username']; host?: User['host'] | null; limit?: number; sinceId?: Following['id']; untilId?: Following['id']; }; res: FollowingFollowerPopulated[]; };
+ 'users/following': { req: { userId?: User['id']; username?: User['username']; host?: User['host'] | null; limit?: number; sinceId?: Following['id']; untilId?: Following['id']; }; res: FollowingFolloweePopulated[]; };
+ 'users/gallery/posts': { req: TODO; res: TODO; };
+ 'users/get-frequently-replied-users': { req: TODO; res: TODO; };
+ 'users/groups/create': { req: TODO; res: TODO; };
+ 'users/groups/delete': { req: { groupId: UserGroup['id'] }; res: null; };
+ 'users/groups/invitations/accept': { req: TODO; res: TODO; };
+ 'users/groups/invitations/reject': { req: TODO; res: TODO; };
+ 'users/groups/invite': { req: TODO; res: TODO; };
+ 'users/groups/joined': { req: TODO; res: TODO; };
+ 'users/groups/owned': { req: TODO; res: TODO; };
+ 'users/groups/pull': { req: TODO; res: TODO; };
+ 'users/groups/show': { req: TODO; res: TODO; };
+ 'users/groups/transfer': { req: TODO; res: TODO; };
+ 'users/groups/update': { req: TODO; res: TODO; };
+ 'users/lists/create': { req: { name: string; }; res: UserList; };
+ 'users/lists/delete': { req: { listId: UserList['id']; }; res: null; };
+ 'users/lists/list': { req: NoParams; res: UserList[]; };
+ 'users/lists/pull': { req: { listId: UserList['id']; userId: User['id']; }; res: null; };
+ 'users/lists/push': { req: { listId: UserList['id']; userId: User['id']; }; res: null; };
+ 'users/lists/show': { req: { listId: UserList['id']; }; res: UserList; };
+ 'users/lists/update': { req: { listId: UserList['id']; name: string; }; res: UserList; };
+ 'users/notes': { req: { userId: User['id']; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; };
+ 'users/pages': { req: TODO; res: TODO; };
+ 'users/recommendation': { req: TODO; res: TODO; };
+ 'users/relation': { req: TODO; res: TODO; };
+ 'users/report-abuse': { req: TODO; res: TODO; };
+ 'users/search-by-username-and-host': { req: TODO; res: TODO; };
+ 'users/search': { req: TODO; res: TODO; };
+ 'users/show': { req: ShowUserReq | { userIds: User['id'][]; }; res: {
+ $switch: {
+ $cases: [[
+ { userIds: User['id'][]; },
+ UserDetailed[],
+ ]];
+ $default: UserDetailed;
+ };
+ }; };
+ 'users/stats': { req: TODO; res: TODO; };
+};
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
new file mode 100644
index 0000000000..261ecd33f4
--- /dev/null
+++ b/packages/misskey-js/src/consts.ts
@@ -0,0 +1,42 @@
+export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
+
+export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
+
+export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
+
+export const ffVisibility = ['public', 'followers', 'private'] as const;
+
+export const permissions = [
+ 'read:account',
+ 'write:account',
+ 'read:blocks',
+ 'write:blocks',
+ 'read:drive',
+ 'write:drive',
+ 'read:favorites',
+ 'write:favorites',
+ 'read:following',
+ 'write:following',
+ 'read:messaging',
+ 'write:messaging',
+ 'read:mutes',
+ 'write:mutes',
+ 'write:notes',
+ 'read:notifications',
+ 'write:notifications',
+ 'read:reactions',
+ 'write:reactions',
+ 'write:votes',
+ 'read:pages',
+ 'write:pages',
+ 'write:page-likes',
+ 'read:page-likes',
+ 'read:user-groups',
+ 'write:user-groups',
+ 'read:channels',
+ 'write:channels',
+ 'read:gallery',
+ 'write:gallery',
+ 'read:gallery-likes',
+ 'write:gallery-likes',
+];
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
new file mode 100644
index 0000000000..37a8bc6184
--- /dev/null
+++ b/packages/misskey-js/src/entities.ts
@@ -0,0 +1,508 @@
+export type ID = string;
+export type DateString = string;
+
+type TODO = Record<string, any>;
+
+// NOTE: 極力この型を使うのは避け、UserLite か UserDetailed か明示するように
+export type User = UserLite | UserDetailed;
+
+export type UserLite = {
+ id: ID;
+ username: string;
+ host: string | null;
+ name: string;
+ onlineStatus: 'online' | 'active' | 'offline' | 'unknown';
+ avatarUrl: string;
+ avatarBlurhash: string;
+ emojis: {
+ name: string;
+ url: string;
+ }[];
+ instance?: {
+ name: Instance['name'];
+ softwareName: Instance['softwareName'];
+ softwareVersion: Instance['softwareVersion'];
+ iconUrl: Instance['iconUrl'];
+ faviconUrl: Instance['faviconUrl'];
+ themeColor: Instance['themeColor'];
+ };
+};
+
+export type UserDetailed = UserLite & {
+ bannerBlurhash: string | null;
+ bannerColor: string | null;
+ bannerUrl: string | null;
+ birthday: string | null;
+ createdAt: DateString;
+ description: string | null;
+ ffVisibility: 'public' | 'followers' | 'private';
+ fields: {name: string; value: string}[];
+ followersCount: number;
+ followingCount: number;
+ hasPendingFollowRequestFromYou: boolean;
+ hasPendingFollowRequestToYou: boolean;
+ isAdmin: boolean;
+ isBlocked: boolean;
+ isBlocking: boolean;
+ isBot: boolean;
+ isCat: boolean;
+ isFollowed: boolean;
+ isFollowing: boolean;
+ isLocked: boolean;
+ isModerator: boolean;
+ isMuted: boolean;
+ isSilenced: boolean;
+ isSuspended: boolean;
+ lang: string | null;
+ lastFetchedAt?: DateString;
+ location: string | null;
+ notesCount: number;
+ pinnedNoteIds: ID[];
+ pinnedNotes: Note[];
+ pinnedPage: Page | null;
+ pinnedPageId: string | null;
+ publicReactions: boolean;
+ securityKeys: boolean;
+ twoFactorEnabled: boolean;
+ updatedAt: DateString | null;
+ uri: string | null;
+ url: string | null;
+};
+
+export type UserGroup = TODO;
+
+export type UserList = {
+ id: ID;
+ createdAt: DateString;
+ name: string;
+ userIds: User['id'][];
+};
+
+export type MeDetailed = UserDetailed & {
+ avatarId: DriveFile['id'];
+ bannerId: DriveFile['id'];
+ autoAcceptFollowed: boolean;
+ alwaysMarkNsfw: boolean;
+ carefulBot: boolean;
+ emailNotificationTypes: string[];
+ hasPendingReceivedFollowRequest: boolean;
+ hasUnreadAnnouncement: boolean;
+ hasUnreadAntenna: boolean;
+ hasUnreadChannel: boolean;
+ hasUnreadMentions: boolean;
+ hasUnreadMessagingMessage: boolean;
+ hasUnreadNotification: boolean;
+ hasUnreadSpecifiedNotes: boolean;
+ hideOnlineStatus: boolean;
+ injectFeaturedNote: boolean;
+ integrations: Record<string, any>;
+ isDeleted: boolean;
+ isExplorable: boolean;
+ mutedWords: string[][];
+ mutingNotificationTypes: string[];
+ noCrawle: boolean;
+ receiveAnnouncementEmail: boolean;
+ usePasswordLessLogin: boolean;
+ [other: string]: any;
+};
+
+export type DriveFile = {
+ id: ID;
+ createdAt: DateString;
+ isSensitive: boolean;
+ name: string;
+ thumbnailUrl: string;
+ url: string;
+ type: string;
+ size: number;
+ md5: string;
+ blurhash: string;
+ comment: string | null;
+ properties: Record<string, any>;
+};
+
+export type DriveFolder = TODO;
+
+export type GalleryPost = {
+ id: ID;
+ createdAt: DateString;
+ updatedAt: DateString;
+ userId: User['id'];
+ user: User;
+ title: string;
+ description: string | null;
+ fileIds: DriveFile['id'][];
+ files: DriveFile[];
+ isSensitive: boolean;
+ likedCount: number;
+ isLiked?: boolean;
+};
+
+export type Note = {
+ id: ID;
+ createdAt: DateString;
+ text: string | null;
+ cw: string | null;
+ user: User;
+ userId: User['id'];
+ reply?: Note;
+ replyId: Note['id'];
+ renote?: Note;
+ renoteId: Note['id'];
+ files: DriveFile[];
+ fileIds: DriveFile['id'][];
+ visibility: 'public' | 'home' | 'followers' | 'specified';
+ visibleUserIds?: User['id'][];
+ localOnly?: boolean;
+ myReaction?: string;
+ reactions: Record<string, number>;
+ renoteCount: number;
+ repliesCount: number;
+ poll?: {
+ expiresAt: DateString | null;
+ multiple: boolean;
+ choices: {
+ isVoted: boolean;
+ text: string;
+ votes: number;
+ }[];
+ };
+ emojis: {
+ name: string;
+ url: string;
+ }[];
+ uri?: string;
+ url?: string;
+ isHidden?: boolean;
+};
+
+export type NoteReaction = {
+ id: ID;
+ createdAt: DateString;
+ user: UserLite;
+ type: string;
+};
+
+export type Notification = {
+ id: ID;
+ createdAt: DateString;
+ isRead: boolean;
+} & ({
+ type: 'reaction';
+ reaction: string;
+ user: User;
+ userId: User['id'];
+ note: Note;
+} | {
+ type: 'reply';
+ user: User;
+ userId: User['id'];
+ note: Note;
+} | {
+ type: 'renote';
+ user: User;
+ userId: User['id'];
+ note: Note;
+} | {
+ type: 'quote';
+ user: User;
+ userId: User['id'];
+ note: Note;
+} | {
+ type: 'mention';
+ user: User;
+ userId: User['id'];
+ note: Note;
+} | {
+ type: 'pollVote';
+ user: User;
+ userId: User['id'];
+ note: Note;
+} | {
+ type: 'follow';
+ user: User;
+ userId: User['id'];
+} | {
+ type: 'followRequestAccepted';
+ user: User;
+ userId: User['id'];
+} | {
+ type: 'receiveFollowRequest';
+ user: User;
+ userId: User['id'];
+} | {
+ type: 'groupInvited';
+ invitation: UserGroup;
+ user: User;
+ userId: User['id'];
+} | {
+ type: 'app';
+ header?: string | null;
+ body: string;
+ icon?: string | null;
+});
+
+export type MessagingMessage = {
+ id: ID;
+ createdAt: DateString;
+ file: DriveFile | null;
+ fileId: DriveFile['id'] | null;
+ isRead: boolean;
+ reads: User['id'][];
+ text: string | null;
+ user: User;
+ userId: User['id'];
+ recipient?: User | null;
+ recipientId: User['id'] | null;
+ group?: UserGroup | null;
+ groupId: UserGroup['id'] | null;
+};
+
+export type CustomEmoji = {
+ id: string;
+ name: string;
+ url: string;
+ category: string;
+ aliases: string[];
+};
+
+export type LiteInstanceMetadata = {
+ maintainerName: string | null;
+ maintainerEmail: string | null;
+ version: string;
+ name: string | null;
+ uri: string;
+ description: string | null;
+ langs: string[];
+ tosUrl: string | null;
+ repositoryUrl: string;
+ feedbackUrl: string;
+ disableRegistration: boolean;
+ disableLocalTimeline: boolean;
+ disableGlobalTimeline: boolean;
+ driveCapacityPerLocalUserMb: number;
+ driveCapacityPerRemoteUserMb: number;
+ emailRequiredForSignup: boolean;
+ enableHcaptcha: boolean;
+ hcaptchaSiteKey: string | null;
+ enableRecaptcha: boolean;
+ recaptchaSiteKey: string | null;
+ enableTurnstile: boolean;
+ turnstileSiteKey: string | null;
+ swPublickey: string | null;
+ themeColor: string | null;
+ mascotImageUrl: string | null;
+ bannerUrl: string | null;
+ errorImageUrl: string | null;
+ iconUrl: string | null;
+ backgroundImageUrl: string | null;
+ logoImageUrl: string | null;
+ maxNoteTextLength: number;
+ enableEmail: boolean;
+ enableTwitterIntegration: boolean;
+ enableGithubIntegration: boolean;
+ enableDiscordIntegration: boolean;
+ enableServiceWorker: boolean;
+ emojis: CustomEmoji[];
+ defaultDarkTheme: string | null;
+ defaultLightTheme: string | null;
+ ads: {
+ id: ID;
+ ratio: number;
+ place: string;
+ url: string;
+ imageUrl: string;
+ }[];
+ translatorAvailable: boolean;
+};
+
+export type DetailedInstanceMetadata = LiteInstanceMetadata & {
+ pinnedPages: string[];
+ pinnedClipId: string | null;
+ cacheRemoteFiles: boolean;
+ requireSetup: boolean;
+ proxyAccountName: string | null;
+ features: Record<string, any>;
+};
+
+export type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata;
+
+export type ServerInfo = {
+ machine: string;
+ cpu: {
+ model: string;
+ cores: number;
+ };
+ mem: {
+ total: number;
+ };
+ fs: {
+ total: number;
+ used: number;
+ };
+};
+
+export type Stats = {
+ notesCount: number;
+ originalNotesCount: number;
+ usersCount: number;
+ originalUsersCount: number;
+ instances: number;
+ driveUsageLocal: number;
+ driveUsageRemote: number;
+};
+
+export type Page = {
+ id: ID;
+ createdAt: DateString;
+ updatedAt: DateString;
+ userId: User['id'];
+ user: User;
+ content: Record<string, any>[];
+ variables: Record<string, any>[];
+ title: string;
+ name: string;
+ summary: string | null;
+ hideTitleWhenPinned: boolean;
+ alignCenter: boolean;
+ font: string;
+ script: string;
+ eyeCatchingImageId: DriveFile['id'] | null;
+ eyeCatchingImage: DriveFile | null;
+ attachedFiles: any;
+ likedCount: number;
+ isLiked?: boolean;
+};
+
+export type PageEvent = {
+ pageId: Page['id'];
+ event: string;
+ var: any;
+ userId: User['id'];
+ user: User;
+};
+
+export type Announcement = {
+ id: ID;
+ createdAt: DateString;
+ updatedAt: DateString | null;
+ text: string;
+ title: string;
+ imageUrl: string | null;
+ isRead?: boolean;
+};
+
+export type Antenna = {
+ id: ID;
+ createdAt: DateString;
+ name: string;
+ keywords: string[][]; // TODO
+ excludeKeywords: string[][]; // TODO
+ src: 'home' | 'all' | 'users' | 'list' | 'group';
+ userListId: ID | null; // TODO
+ userGroupId: ID | null; // TODO
+ users: string[]; // TODO
+ caseSensitive: boolean;
+ notify: boolean;
+ withReplies: boolean;
+ withFile: boolean;
+ hasUnreadNote: boolean;
+};
+
+export type App = TODO;
+
+export type AuthSession = {
+ id: ID;
+ app: App;
+ token: string;
+};
+
+export type Ad = TODO;
+
+export type Clip = TODO;
+
+export type NoteFavorite = {
+ id: ID;
+ createdAt: DateString;
+ noteId: Note['id'];
+ note: Note;
+};
+
+export type FollowRequest = {
+ id: ID;
+ follower: User;
+ followee: User;
+};
+
+export type Channel = {
+ id: ID;
+ // TODO
+};
+
+export type Following = {
+ id: ID;
+ createdAt: DateString;
+ followerId: User['id'];
+ followeeId: User['id'];
+};
+
+export type FollowingFolloweePopulated = Following & {
+ followee: UserDetailed;
+};
+
+export type FollowingFollowerPopulated = Following & {
+ follower: UserDetailed;
+};
+
+export type Blocking = {
+ id: ID;
+ createdAt: DateString;
+ blockeeId: User['id'];
+ blockee: UserDetailed;
+};
+
+export type Instance = {
+ id: ID;
+ caughtAt: DateString;
+ host: string;
+ usersCount: number;
+ notesCount: number;
+ followingCount: number;
+ followersCount: number;
+ driveUsage: number;
+ driveFiles: number;
+ latestRequestSentAt: DateString | null;
+ latestStatus: number | null;
+ latestRequestReceivedAt: DateString | null;
+ lastCommunicatedAt: DateString;
+ isNotResponding: boolean;
+ isSuspended: boolean;
+ softwareName: string | null;
+ softwareVersion: string | null;
+ openRegistrations: boolean | null;
+ name: string | null;
+ description: string | null;
+ maintainerName: string | null;
+ maintainerEmail: string | null;
+ iconUrl: string | null;
+ faviconUrl: string | null;
+ themeColor: string | null;
+ infoUpdatedAt: DateString | null;
+};
+
+export type Signin = {
+ id: ID;
+ createdAt: DateString;
+ ip: string;
+ headers: Record<string, any>;
+ success: boolean;
+};
+
+export type UserSorting =
+ | '+follower'
+ | '-follower'
+ | '+createdAt'
+ | '-createdAt'
+ | '+updatedAt'
+ | '-updatedAt';
+export type OriginType = 'combined' | 'local' | 'remote';
diff --git a/packages/misskey-js/src/index.ts b/packages/misskey-js/src/index.ts
new file mode 100644
index 0000000000..f431d65cc7
--- /dev/null
+++ b/packages/misskey-js/src/index.ts
@@ -0,0 +1,26 @@
+import { Endpoints } from './api.types';
+import Stream, { Connection } from './streaming';
+import { Channels } from './streaming.types';
+import { Acct } from './acct';
+import * as consts from './consts';
+
+export {
+ Endpoints,
+ Stream,
+ Connection as ChannelConnection,
+ Channels,
+ Acct,
+};
+
+export const permissions = consts.permissions;
+export const notificationTypes = consts.notificationTypes;
+export const noteVisibilities = consts.noteVisibilities;
+export const mutedNoteReasons = consts.mutedNoteReasons;
+export const ffVisibility = consts.ffVisibility;
+
+// api extractor not supported yet
+//export * as api from './api';
+//export * as entities from './entities';
+import * as api from './api';
+import * as entities from './entities';
+export { api, entities };
diff --git a/packages/misskey-js/src/streaming.ts b/packages/misskey-js/src/streaming.ts
new file mode 100644
index 0000000000..92b83192cf
--- /dev/null
+++ b/packages/misskey-js/src/streaming.ts
@@ -0,0 +1,340 @@
+import autobind from 'autobind-decorator';
+import { EventEmitter } from 'eventemitter3';
+import ReconnectingWebsocket from 'reconnecting-websocket';
+import { BroadcastEvents, Channels } from './streaming.types';
+
+export function urlQuery(obj: Record<string, string | number | boolean | undefined>): string {
+ const params = Object.entries(obj)
+ .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ .reduce((a, [k, v]) => (a[k] = v!, a), {} as Record<string, string | number | boolean>);
+
+ return Object.entries(params)
+ .map((e) => `${e[0]}=${encodeURIComponent(e[1])}`)
+ .join('&');
+}
+
+type AnyOf<T extends Record<any, any>> = T[keyof T];
+
+type StreamEvents = {
+ _connected_: void;
+ _disconnected_: void;
+} & BroadcastEvents;
+
+/**
+ * Misskey stream connection
+ */
+export default class Stream extends EventEmitter<StreamEvents> {
+ private stream: ReconnectingWebsocket;
+ public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing';
+ private sharedConnectionPools: Pool[] = [];
+ private sharedConnections: SharedConnection[] = [];
+ private nonSharedConnections: NonSharedConnection[] = [];
+ private idCounter = 0;
+
+ constructor(origin: string, user: { token: string; } | null, options?: {
+ WebSocket?: any;
+ }) {
+ super();
+ options = options || { };
+
+ const query = urlQuery({
+ i: user?.token,
+
+ // To prevent cache of an HTML such as error screen
+ _t: Date.now(),
+ });
+
+ const wsOrigin = origin.replace('http://', 'ws://').replace('https://', 'wss://');
+
+ this.stream = new ReconnectingWebsocket(`${wsOrigin}/streaming?${query}`, '', {
+ minReconnectionDelay: 1, // https://github.com/pladaria/reconnecting-websocket/issues/91
+ WebSocket: options.WebSocket,
+ });
+ this.stream.addEventListener('open', this.onOpen);
+ this.stream.addEventListener('close', this.onClose);
+ this.stream.addEventListener('message', this.onMessage);
+ }
+
+ @autobind
+ private genId(): string {
+ return (++this.idCounter).toString();
+ }
+
+ @autobind
+ public useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): Connection<Channels[C]> {
+ if (params) {
+ return this.connectToChannel(channel, params);
+ } else {
+ return this.useSharedConnection(channel, name);
+ }
+ }
+
+ @autobind
+ private useSharedConnection<C extends keyof Channels>(channel: C, name?: string): SharedConnection<Channels[C]> {
+ let pool = this.sharedConnectionPools.find(p => p.channel === channel);
+
+ if (pool == null) {
+ pool = new Pool(this, channel, this.genId());
+ this.sharedConnectionPools.push(pool);
+ }
+
+ const connection = new SharedConnection(this, channel, pool, name);
+ this.sharedConnections.push(connection);
+ return connection;
+ }
+
+ @autobind
+ public removeSharedConnection(connection: SharedConnection): void {
+ this.sharedConnections = this.sharedConnections.filter(c => c !== connection);
+ }
+
+ @autobind
+ public removeSharedConnectionPool(pool: Pool): void {
+ this.sharedConnectionPools = this.sharedConnectionPools.filter(p => p !== pool);
+ }
+
+ @autobind
+ private connectToChannel<C extends keyof Channels>(channel: C, params: Channels[C]['params']): NonSharedConnection<Channels[C]> {
+ const connection = new NonSharedConnection(this, channel, this.genId(), params);
+ this.nonSharedConnections.push(connection);
+ return connection;
+ }
+
+ @autobind
+ public disconnectToChannel(connection: NonSharedConnection): void {
+ this.nonSharedConnections = this.nonSharedConnections.filter(c => c !== connection);
+ }
+
+ /**
+ * Callback of when open connection
+ */
+ @autobind
+ private onOpen(): void {
+ const isReconnect = this.state === 'reconnecting';
+
+ this.state = 'connected';
+ this.emit('_connected_');
+
+ // チャンネル再接続
+ if (isReconnect) {
+ for (const p of this.sharedConnectionPools) p.connect();
+ for (const c of this.nonSharedConnections) c.connect();
+ }
+ }
+
+ /**
+ * Callback of when close connection
+ */
+ @autobind
+ private onClose(): void {
+ if (this.state === 'connected') {
+ this.state = 'reconnecting';
+ this.emit('_disconnected_');
+ }
+ }
+
+ /**
+ * Callback of when received a message from connection
+ */
+ @autobind
+ private onMessage(message: { data: string; }): void {
+ const { type, body } = JSON.parse(message.data);
+
+ if (type === 'channel') {
+ const id = body.id;
+
+ let connections: Connection[];
+
+ connections = this.sharedConnections.filter(c => c.id === id);
+
+ if (connections.length === 0) {
+ const found = this.nonSharedConnections.find(c => c.id === id);
+ if (found) {
+ connections = [found];
+ }
+ }
+
+ for (const c of connections) {
+ c.emit(body.type, body.body);
+ c.inCount++;
+ }
+ } else {
+ this.emit(type, body);
+ }
+ }
+
+ /**
+ * Send a message to connection
+ */
+ @autobind
+ public send(typeOrPayload: any, payload?: any): void {
+ const data = payload === undefined ? typeOrPayload : {
+ type: typeOrPayload,
+ body: payload,
+ };
+
+ this.stream.send(JSON.stringify(data));
+ }
+
+ /**
+ * Close this connection
+ */
+ @autobind
+ public close(): void {
+ this.stream.close();
+ }
+}
+
+// TODO: これらのクラスを Stream クラスの内部クラスにすれば余計なメンバをpublicにしないで済むかも?
+// もしくは @internal を使う? https://www.typescriptlang.org/tsconfig#stripInternal
+class Pool {
+ public channel: string;
+ public id: string;
+ protected stream: Stream;
+ public users = 0;
+ private disposeTimerId: any;
+ private isConnected = false;
+
+ constructor(stream: Stream, channel: string, id: string) {
+ this.channel = channel;
+ this.stream = stream;
+ this.id = id;
+
+ this.stream.on('_disconnected_', this.onStreamDisconnected);
+ }
+
+ @autobind
+ private onStreamDisconnected(): void {
+ this.isConnected = false;
+ }
+
+ @autobind
+ public inc(): void {
+ if (this.users === 0 && !this.isConnected) {
+ this.connect();
+ }
+
+ this.users++;
+
+ // タイマー解除
+ if (this.disposeTimerId) {
+ clearTimeout(this.disposeTimerId);
+ this.disposeTimerId = null;
+ }
+ }
+
+ @autobind
+ public dec(): void {
+ this.users--;
+
+ // そのコネクションの利用者が誰もいなくなったら
+ if (this.users === 0) {
+ // また直ぐに再利用される可能性があるので、一定時間待ち、
+ // 新たな利用者が現れなければコネクションを切断する
+ this.disposeTimerId = setTimeout(() => {
+ this.disconnect();
+ }, 3000);
+ }
+ }
+
+ @autobind
+ public connect(): void {
+ if (this.isConnected) return;
+ this.isConnected = true;
+ this.stream.send('connect', {
+ channel: this.channel,
+ id: this.id,
+ });
+ }
+
+ @autobind
+ private disconnect(): void {
+ this.stream.off('_disconnected_', this.onStreamDisconnected);
+ this.stream.send('disconnect', { id: this.id });
+ this.stream.removeSharedConnectionPool(this);
+ }
+}
+
+export abstract class Connection<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> {
+ public channel: string;
+ protected stream: Stream;
+ public abstract id: string;
+
+ public name?: string; // for debug
+ public inCount = 0; // for debug
+ public outCount = 0; // for debug
+
+ constructor(stream: Stream, channel: string, name?: string) {
+ super();
+
+ this.stream = stream;
+ this.channel = channel;
+ this.name = name;
+ }
+
+ @autobind
+ public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void {
+ this.stream.send('ch', {
+ id: this.id,
+ type: type,
+ body: body,
+ });
+
+ this.outCount++;
+ }
+
+ public abstract dispose(): void;
+}
+
+class SharedConnection<Channel extends AnyOf<Channels> = any> extends Connection<Channel> {
+ private pool: Pool;
+
+ public get id(): string {
+ return this.pool.id;
+ }
+
+ constructor(stream: Stream, channel: string, pool: Pool, name?: string) {
+ super(stream, channel, name);
+
+ this.pool = pool;
+ this.pool.inc();
+ }
+
+ @autobind
+ public dispose(): void {
+ this.pool.dec();
+ this.removeAllListeners();
+ this.stream.removeSharedConnection(this);
+ }
+}
+
+class NonSharedConnection<Channel extends AnyOf<Channels> = any> extends Connection<Channel> {
+ public id: string;
+ protected params: Channel['params'];
+
+ constructor(stream: Stream, channel: string, id: string, params: Channel['params']) {
+ super(stream, channel);
+
+ this.params = params;
+ this.id = id;
+
+ this.connect();
+ }
+
+ @autobind
+ public connect(): void {
+ this.stream.send('connect', {
+ channel: this.channel,
+ id: this.id,
+ params: this.params,
+ });
+ }
+
+ @autobind
+ public dispose(): void {
+ this.removeAllListeners();
+ this.stream.send('disconnect', { id: this.id });
+ this.stream.disconnectToChannel(this);
+ }
+}
diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts
new file mode 100644
index 0000000000..f711b8d6eb
--- /dev/null
+++ b/packages/misskey-js/src/streaming.types.ts
@@ -0,0 +1,152 @@
+import { Antenna, CustomEmoji, DriveFile, MeDetailed, MessagingMessage, Note, Notification, PageEvent, User, UserGroup } from './entities';
+
+type FIXME = any;
+
+export type Channels = {
+ main: {
+ params: null;
+ events: {
+ notification: (payload: Notification) => void;
+ mention: (payload: Note) => void;
+ reply: (payload: Note) => void;
+ renote: (payload: Note) => void;
+ follow: (payload: User) => void; // 自分が他人をフォローしたとき
+ followed: (payload: User) => void; // 他人が自分をフォローしたとき
+ unfollow: (payload: User) => void; // 自分が他人をフォロー解除したとき
+ meUpdated: (payload: MeDetailed) => void;
+ pageEvent: (payload: PageEvent) => void;
+ urlUploadFinished: (payload: { marker: string; file: DriveFile; }) => void;
+ readAllNotifications: () => void;
+ unreadNotification: (payload: Notification) => void;
+ unreadMention: (payload: Note['id']) => void;
+ readAllUnreadMentions: () => void;
+ unreadSpecifiedNote: (payload: Note['id']) => void;
+ readAllUnreadSpecifiedNotes: () => void;
+ readAllMessagingMessages: () => void;
+ messagingMessage: (payload: MessagingMessage) => void;
+ unreadMessagingMessage: (payload: MessagingMessage) => void;
+ readAllAntennas: () => void;
+ unreadAntenna: (payload: Antenna) => void;
+ readAllAnnouncements: () => void;
+ readAllChannels: () => void;
+ unreadChannel: (payload: Note['id']) => void;
+ myTokenRegenerated: () => void;
+ reversiNoInvites: () => void;
+ reversiInvited: (payload: FIXME) => void;
+ signin: (payload: FIXME) => void;
+ registryUpdated: (payload: {
+ scope?: string[];
+ key: string;
+ value: any | null;
+ }) => void;
+ driveFileCreated: (payload: DriveFile) => void;
+ readAntenna: (payload: Antenna) => void;
+ };
+ receives: null;
+ };
+ homeTimeline: {
+ params: null;
+ events: {
+ note: (payload: Note) => void;
+ };
+ receives: null;
+ };
+ localTimeline: {
+ params: null;
+ events: {
+ note: (payload: Note) => void;
+ };
+ receives: null;
+ };
+ hybridTimeline: {
+ params: null;
+ events: {
+ note: (payload: Note) => void;
+ };
+ receives: null;
+ };
+ globalTimeline: {
+ params: null;
+ events: {
+ note: (payload: Note) => void;
+ };
+ receives: null;
+ };
+ messaging: {
+ params: {
+ otherparty?: User['id'] | null;
+ group?: UserGroup['id'] | null;
+ };
+ events: {
+ message: (payload: MessagingMessage) => void;
+ deleted: (payload: MessagingMessage['id']) => void;
+ read: (payload: MessagingMessage['id'][]) => void;
+ typers: (payload: User[]) => void;
+ };
+ receives: {
+ read: {
+ id: MessagingMessage['id'];
+ };
+ };
+ };
+ serverStats: {
+ params: null;
+ events: {
+ stats: (payload: FIXME) => void;
+ };
+ receives: {
+ requestLog: {
+ id: string | number;
+ length: number;
+ };
+ };
+ };
+ queueStats: {
+ params: null;
+ events: {
+ stats: (payload: FIXME) => void;
+ };
+ receives: {
+ requestLog: {
+ id: string | number;
+ length: number;
+ };
+ };
+ };
+};
+
+export type NoteUpdatedEvent = {
+ id: Note['id'];
+ type: 'reacted';
+ body: {
+ reaction: string;
+ userId: User['id'];
+ };
+} | {
+ id: Note['id'];
+ type: 'unreacted';
+ body: {
+ reaction: string;
+ userId: User['id'];
+ };
+} | {
+ id: Note['id'];
+ type: 'deleted';
+ body: {
+ deletedAt: string;
+ };
+} | {
+ id: Note['id'];
+ type: 'pollVoted';
+ body: {
+ choice: number;
+ userId: User['id'];
+ };
+};
+
+export type BroadcastEvents = {
+ noteUpdated: (payload: NoteUpdatedEvent) => void;
+ emojiAdded: (payload: {
+ emoji: CustomEmoji;
+ }) => void;
+};