chore: rome formatting

This commit is contained in:
Kainoa Kanter 2023-04-07 16:47:04 -07:00
parent 37c0423da6
commit aeb0839da9
15 changed files with 1664 additions and 1032 deletions

View File

@ -1,57 +1,65 @@
module.exports = { module.exports = {
root: true, root: true,
parser: '@typescript-eslint/parser', parser: "@typescript-eslint/parser",
parserOptions: { parserOptions: {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.json'], project: ["./tsconfig.json"],
}, },
plugins: [ plugins: ["@typescript-eslint"],
'@typescript-eslint', extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
rules: { rules: {
'indent': ['error', 'tab', { indent: [
'SwitchCase': 1, "error",
'MemberExpression': 'off', "tab",
'flatTernaryExpressions': true, {
'ArrayExpression': 'first', SwitchCase: 1,
'ObjectExpression': 'first', MemberExpression: "off",
}], flatTernaryExpressions: true,
'eol-last': ['error', 'always'], ArrayExpression: "first",
'semi': ['error', 'always'], ObjectExpression: "first",
'quotes': ['error', 'single'], },
'comma-dangle': ['error', 'always-multiline'], ],
'keyword-spacing': ['error', { "eol-last": ["error", "always"],
'before': true, semi: ["error", "always"],
'after': true, quotes: ["error", "single"],
}], "comma-dangle": ["error", "always-multiline"],
'key-spacing': ['error', { "keyword-spacing": [
'beforeColon': false, "error",
'afterColon': true, {
}], before: true,
'space-infix-ops': ['error'], after: true,
'space-before-blocks': ['error', 'always'], },
'object-curly-spacing': ['error', 'always'], ],
'nonblock-statement-body-position': ['error', 'beside'], "key-spacing": [
'eqeqeq': ['error', 'always', { 'null': 'ignore' }], "error",
'no-multiple-empty-lines': ['error', { 'max': 1 }], {
'no-multi-spaces': ['error'], beforeColon: false,
'no-var': ['error'], afterColon: true,
'prefer-arrow-callback': ['error'], },
'no-throw-literal': ['error'], ],
'no-param-reassign': ['warn'], "space-infix-ops": ["error"],
'no-constant-condition': ['warn'], "space-before-blocks": ["error", "always"],
'no-empty-pattern': ['warn'], "object-curly-spacing": ["error", "always"],
'@typescript-eslint/no-unnecessary-condition': ['error'], "nonblock-statement-body-position": ["error", "beside"],
'@typescript-eslint/no-inferrable-types': ['warn'], eqeqeq: ["error", "always", { null: "ignore" }],
'@typescript-eslint/no-non-null-assertion': ['warn'], "no-multiple-empty-lines": ["error", { max: 1 }],
'@typescript-eslint/explicit-function-return-type': ['warn'], "no-multi-spaces": ["error"],
'@typescript-eslint/no-misused-promises': ['error', { "no-var": ["error"],
'checksVoidReturn': false, "prefer-arrow-callback": ["error"],
}], "no-throw-literal": ["error"],
'@typescript-eslint/consistent-type-imports': 'error', "no-param-reassign": ["warn"],
"no-constant-condition": ["warn"],
"no-empty-pattern": ["warn"],
"@typescript-eslint/no-unnecessary-condition": ["error"],
"@typescript-eslint/no-inferrable-types": ["warn"],
"@typescript-eslint/no-non-null-assertion": ["warn"],
"@typescript-eslint/explicit-function-return-type": ["warn"],
"@typescript-eslint/no-misused-promises": [
"error",
{
checksVoidReturn: false,
},
],
"@typescript-eslint/consistent-type-imports": "error",
}, },
}; };

View File

@ -1,7 +1,7 @@
/* /*
* For a detailed explanation regarding each configuration property and type check, visit: * For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/en/configuration.html * https://jestjs.io/docs/en/configuration.html
*/ */
export default { export default {
// All imported modules in your tests should be mocked automatically // All imported modules in your tests should be mocked automatically
@ -117,9 +117,7 @@ export default {
// rootDir: undefined, // rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in // A list of paths to directories that Jest should use to search for files in
roots: [ roots: ["<rootDir>"],
"<rootDir>"
],
// Allows you to use a custom runner instead of Jest's default test runner // Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner", // runner: "jest-runner",
@ -149,7 +147,7 @@ export default {
testMatch: [ testMatch: [
"**/__tests__/**/*.[jt]s?(x)", "**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[tj]s?(x)", "**/?(*.)+(spec|test).[tj]s?(x)",
"<rootDir>/test/**/*" "<rootDir>/test/**/*",
], ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
@ -174,7 +172,7 @@ export default {
// A map from regular expressions to paths to transformers // A map from regular expressions to paths to transformers
transform: { transform: {
"^.+\\.(ts|tsx)$": "ts-jest" "^.+\\.(ts|tsx)$": "ts-jest",
}, },
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation

View File

@ -4,8 +4,8 @@ export type Acct = {
}; };
export function parse(acct: string): Acct { export function parse(acct: string): Acct {
if (acct.startsWith('@')) acct = acct.substr(1); if (acct.startsWith("@")) acct = acct.substr(1);
const split = acct.split('@', 2); const split = acct.split("@", 2);
return { username: split[0], host: split[1] || null }; return { username: split[0], host: split[1] || null };
} }

View File

@ -1,4 +1,4 @@
import { Endpoints } from './api.types'; import { Endpoints } from "./api.types";
const MK_API_ERROR = Symbol(); const MK_API_ERROR = Symbol();
@ -6,7 +6,7 @@ export type APIError = {
id: string; id: string;
code: string; code: string;
message: string; message: string;
kind: 'client' | 'server'; kind: "client" | "server";
info: Record<string, any>; info: Record<string, any>;
}; };
@ -14,25 +14,38 @@ export function isAPIError(reason: any): reason is APIError {
return reason[MK_API_ERROR] === true; return reason[MK_API_ERROR] === true;
} }
export type FetchLike = (input: string, init?: { export type FetchLike = (
input: string,
init?: {
method?: string; method?: string;
body?: string; body?: string;
credentials?: RequestCredentials; credentials?: RequestCredentials;
cache?: RequestCache; cache?: RequestCache;
}) => Promise<{ },
status: number; ) => Promise<{
json(): Promise<any>; status: number;
}>; json(): Promise<any>;
}>;
type IsNeverType<T> = [T] extends [never] ? true : false; type IsNeverType<T> = [T] extends [never] ? true : false;
type StrictExtract<Union, Cond> = Cond extends Union ? Union : never; type StrictExtract<Union, Cond> = Cond extends Union ? Union : never;
type IsCaseMatched<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> = type IsCaseMatched<
IsNeverType<StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>> extends false ? true : false; 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> = type GetCaseResult<
StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>[1]; 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 { export class APIClient {
public origin: string; public origin: string;
@ -40,9 +53,9 @@ export class APIClient {
public fetch: FetchLike; public fetch: FetchLike;
constructor(opts: { constructor(opts: {
origin: APIClient['origin']; origin: APIClient["origin"];
credential?: APIClient['credential']; credential?: APIClient["credential"];
fetch?: APIClient['fetch'] | null | undefined; fetch?: APIClient["fetch"] | null | undefined;
}) { }) {
this.origin = opts.origin; this.origin = opts.origin;
this.credential = opts.credential; this.credential = opts.credential;
@ -51,48 +64,64 @@ export class APIClient {
this.fetch = opts.fetch || ((...args) => fetch(...args)); this.fetch = opts.fetch || ((...args) => fetch(...args));
} }
public request<E extends keyof Endpoints, P extends Endpoints[E]['req']>( public request<E extends keyof Endpoints, P extends Endpoints[E]["req"]>(
endpoint: E, params: P = {} as P, credential?: string | null | undefined, endpoint: E,
): Promise<Endpoints[E]['res'] extends { $switch: { $cases: [any, any][]; $default: any; }; } params: P = {} as P,
? credential?: string | null | undefined,
IsCaseMatched<E, P, 0> extends true ? GetCaseResult<E, P, 0> : ): Promise<
IsCaseMatched<E, P, 1> extends true ? GetCaseResult<E, P, 1> : Endpoints[E]["res"] extends {
IsCaseMatched<E, P, 2> extends true ? GetCaseResult<E, P, 2> : $switch: { $cases: [any, any][]; $default: any };
IsCaseMatched<E, P, 3> extends true ? GetCaseResult<E, P, 3> : }
IsCaseMatched<E, P, 4> extends true ? GetCaseResult<E, P, 4> : ? IsCaseMatched<E, P, 0> extends true
IsCaseMatched<E, P, 5> extends true ? GetCaseResult<E, P, 5> : ? GetCaseResult<E, P, 0>
IsCaseMatched<E, P, 6> extends true ? GetCaseResult<E, P, 6> : : IsCaseMatched<E, P, 1> extends true
IsCaseMatched<E, P, 7> extends true ? GetCaseResult<E, P, 7> : ? GetCaseResult<E, P, 1>
IsCaseMatched<E, P, 8> extends true ? GetCaseResult<E, P, 8> : : IsCaseMatched<E, P, 2> extends true
IsCaseMatched<E, P, 9> extends true ? GetCaseResult<E, P, 9> : ? GetCaseResult<E, P, 2>
Endpoints[E]['res']['$switch']['$default'] : IsCaseMatched<E, P, 3> extends true
: Endpoints[E]['res']> ? 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) => { const promise = new Promise((resolve, reject) => {
this.fetch(`${this.origin}/api/${endpoint}`, { this.fetch(`${this.origin}/api/${endpoint}`, {
method: 'POST', method: "POST",
body: JSON.stringify({ body: JSON.stringify({
...params, ...params,
i: credential !== undefined ? credential : this.credential, i: credential !== undefined ? credential : this.credential,
}), }),
credentials: 'omit', credentials: "omit",
cache: 'no-cache', cache: "no-cache",
}).then(async (res) => { })
const body = res.status === 204 ? null : await res.json(); .then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body); if (res.status === 200) {
} else if (res.status === 204) { resolve(body);
resolve(null); } else if (res.status === 204) {
} else { resolve(null);
reject({ } else {
[MK_API_ERROR]: true, reject({
...body.error, [MK_API_ERROR]: true,
}); ...body.error,
} });
}).catch(reject); }
})
.catch(reject);
}); });
return promise as any; return promise as any;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,60 @@
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const; 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 noteVisibilities = [
"public",
"home",
"followers",
"specified",
] as const;
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; export const mutedNoteReasons = ["word", "manual", "spam", "other"] as const;
export const ffVisibility = ['public', 'followers', 'private'] as const; export const ffVisibility = ["public", "followers", "private"] as const;
export const permissions = [ export const permissions = [
'read:account', "read:account",
'write:account', "write:account",
'read:blocks', "read:blocks",
'write:blocks', "write:blocks",
'read:drive', "read:drive",
'write:drive', "write:drive",
'read:favorites', "read:favorites",
'write:favorites', "write:favorites",
'read:following', "read:following",
'write:following', "write:following",
'read:messaging', "read:messaging",
'write:messaging', "write:messaging",
'read:mutes', "read:mutes",
'write:mutes', "write:mutes",
'write:notes', "write:notes",
'read:notifications', "read:notifications",
'write:notifications', "write:notifications",
'read:reactions', "read:reactions",
'write:reactions', "write:reactions",
'write:votes', "write:votes",
'read:pages', "read:pages",
'write:pages', "write:pages",
'write:page-likes', "write:page-likes",
'read:page-likes', "read:page-likes",
'read:user-groups', "read:user-groups",
'write:user-groups', "write:user-groups",
'read:channels', "read:channels",
'write:channels', "write:channels",
'read:gallery', "read:gallery",
'write:gallery', "write:gallery",
'read:gallery-likes', "read:gallery-likes",
'write:gallery-likes', "write:gallery-likes",
]; ];

View File

@ -11,7 +11,7 @@ export type UserLite = {
username: string; username: string;
host: string | null; host: string | null;
name: string; name: string;
onlineStatus: 'online' | 'active' | 'offline' | 'unknown'; onlineStatus: "online" | "active" | "offline" | "unknown";
avatarUrl: string; avatarUrl: string;
avatarBlurhash: string; avatarBlurhash: string;
alsoKnownAs: string[]; alsoKnownAs: string[];
@ -21,12 +21,12 @@ export type UserLite = {
url: string; url: string;
}[]; }[];
instance?: { instance?: {
name: Instance['name']; name: Instance["name"];
softwareName: Instance['softwareName']; softwareName: Instance["softwareName"];
softwareVersion: Instance['softwareVersion']; softwareVersion: Instance["softwareVersion"];
iconUrl: Instance['iconUrl']; iconUrl: Instance["iconUrl"];
faviconUrl: Instance['faviconUrl']; faviconUrl: Instance["faviconUrl"];
themeColor: Instance['themeColor']; themeColor: Instance["themeColor"];
}; };
}; };
@ -37,8 +37,8 @@ export type UserDetailed = UserLite & {
birthday: string | null; birthday: string | null;
createdAt: DateString; createdAt: DateString;
description: string | null; description: string | null;
ffVisibility: 'public' | 'followers' | 'private'; ffVisibility: "public" | "followers" | "private";
fields: {name: string; value: string}[]; fields: { name: string; value: string }[];
followersCount: number; followersCount: number;
followingCount: number; followingCount: number;
hasPendingFollowRequestFromYou: boolean; hasPendingFollowRequestFromYou: boolean;
@ -77,12 +77,12 @@ export type UserList = {
id: ID; id: ID;
createdAt: DateString; createdAt: DateString;
name: string; name: string;
userIds: User['id'][]; userIds: User["id"][];
}; };
export type MeDetailed = UserDetailed & { export type MeDetailed = UserDetailed & {
avatarId: DriveFile['id']; avatarId: DriveFile["id"];
bannerId: DriveFile['id']; bannerId: DriveFile["id"];
autoAcceptFollowed: boolean; autoAcceptFollowed: boolean;
alwaysMarkNsfw: boolean; alwaysMarkNsfw: boolean;
carefulBot: boolean; carefulBot: boolean;
@ -133,15 +133,15 @@ export type Note = {
text: string | null; text: string | null;
cw: string | null; cw: string | null;
user: User; user: User;
userId: User['id']; userId: User["id"];
reply?: Note; reply?: Note;
replyId: Note['id']; replyId: Note["id"];
renote?: Note; renote?: Note;
renoteId: Note['id']; renoteId: Note["id"];
files: DriveFile[]; files: DriveFile[];
fileIds: DriveFile['id'][]; fileIds: DriveFile["id"][];
visibility: 'public' | 'home' | 'followers' | 'specified'; visibility: "public" | "home" | "followers" | "specified";
visibleUserIds?: User['id'][]; visibleUserIds?: User["id"][];
localOnly?: boolean; localOnly?: boolean;
myReaction?: string; myReaction?: string;
reactions: Record<string, number>; reactions: Record<string, number>;
@ -176,75 +176,87 @@ export type Notification = {
id: ID; id: ID;
createdAt: DateString; createdAt: DateString;
isRead: boolean; isRead: boolean;
} & ({ } & (
type: 'reaction'; | {
reaction: string; type: "reaction";
user: User; reaction: string;
userId: User['id']; user: User;
note: Note; userId: User["id"];
} | { note: Note;
type: 'reply'; }
user: User; | {
userId: User['id']; type: "reply";
note: Note; user: User;
} | { userId: User["id"];
type: 'renote'; note: Note;
user: User; }
userId: User['id']; | {
note: Note; type: "renote";
} | { user: User;
type: 'quote'; userId: User["id"];
user: User; note: Note;
userId: User['id']; }
note: Note; | {
} | { type: "quote";
type: 'mention'; user: User;
user: User; userId: User["id"];
userId: User['id']; note: Note;
note: Note; }
} | { | {
type: 'pollVote'; type: "mention";
user: User; user: User;
userId: User['id']; userId: User["id"];
note: Note; note: Note;
} | { }
type: 'follow'; | {
user: User; type: "pollVote";
userId: User['id']; user: User;
} | { userId: User["id"];
type: 'followRequestAccepted'; note: Note;
user: User; }
userId: User['id']; | {
} | { type: "follow";
type: 'receiveFollowRequest'; user: User;
user: User; userId: User["id"];
userId: User['id']; }
} | { | {
type: 'groupInvited'; type: "followRequestAccepted";
invitation: UserGroup; user: User;
user: User; userId: User["id"];
userId: User['id']; }
} | { | {
type: 'app'; type: "receiveFollowRequest";
header?: string | null; user: User;
body: string; userId: User["id"];
icon?: string | null; }
}); | {
type: "groupInvited";
invitation: UserGroup;
user: User;
userId: User["id"];
}
| {
type: "app";
header?: string | null;
body: string;
icon?: string | null;
}
);
export type MessagingMessage = { export type MessagingMessage = {
id: ID; id: ID;
createdAt: DateString; createdAt: DateString;
file: DriveFile | null; file: DriveFile | null;
fileId: DriveFile['id'] | null; fileId: DriveFile["id"] | null;
isRead: boolean; isRead: boolean;
reads: User['id'][]; reads: User["id"][];
text: string | null; text: string | null;
user: User; user: User;
userId: User['id']; userId: User["id"];
recipient?: User | null; recipient?: User | null;
recipientId: User['id'] | null; recipientId: User["id"] | null;
group?: UserGroup | null; group?: UserGroup | null;
groupId: UserGroup['id'] | null; groupId: UserGroup["id"] | null;
}; };
export type CustomEmoji = { export type CustomEmoji = {
@ -325,7 +337,7 @@ export type Page = {
id: ID; id: ID;
createdAt: DateString; createdAt: DateString;
updatedAt: DateString; updatedAt: DateString;
userId: User['id']; userId: User["id"];
user: User; user: User;
content: Record<string, any>[]; content: Record<string, any>[];
variables: Record<string, any>[]; variables: Record<string, any>[];
@ -336,7 +348,7 @@ export type Page = {
alignCenter: boolean; alignCenter: boolean;
font: string; font: string;
script: string; script: string;
eyeCatchingImageId: DriveFile['id'] | null; eyeCatchingImageId: DriveFile["id"] | null;
eyeCatchingImage: DriveFile | null; eyeCatchingImage: DriveFile | null;
attachedFiles: any; attachedFiles: any;
likedCount: number; likedCount: number;
@ -344,10 +356,10 @@ export type Page = {
}; };
export type PageEvent = { export type PageEvent = {
pageId: Page['id']; pageId: Page["id"];
event: string; event: string;
var: any; var: any;
userId: User['id']; userId: User["id"];
user: User; user: User;
}; };
@ -367,7 +379,7 @@ export type Antenna = {
name: string; name: string;
keywords: string[][]; // TODO keywords: string[][]; // TODO
excludeKeywords: string[][]; // TODO excludeKeywords: string[][]; // TODO
src: 'home' | 'all' | 'users' | 'list' | 'group' | 'instances'; src: "home" | "all" | "users" | "list" | "group" | "instances";
userListId: ID | null; // TODO userListId: ID | null; // TODO
userGroupId: ID | null; // TODO userGroupId: ID | null; // TODO
users: string[]; // TODO users: string[]; // TODO
@ -394,7 +406,7 @@ export type Clip = TODO;
export type NoteFavorite = { export type NoteFavorite = {
id: ID; id: ID;
createdAt: DateString; createdAt: DateString;
noteId: Note['id']; noteId: Note["id"];
note: Note; note: Note;
}; };
@ -412,8 +424,8 @@ export type Channel = {
export type Following = { export type Following = {
id: ID; id: ID;
createdAt: DateString; createdAt: DateString;
followerId: User['id']; followerId: User["id"];
followeeId: User['id']; followeeId: User["id"];
}; };
export type FollowingFolloweePopulated = Following & { export type FollowingFolloweePopulated = Following & {
@ -427,7 +439,7 @@ export type FollowingFollowerPopulated = Following & {
export type Blocking = { export type Blocking = {
id: ID; id: ID;
createdAt: DateString; createdAt: DateString;
blockeeId: User['id']; blockeeId: User["id"];
blockee: UserDetailed; blockee: UserDetailed;
}; };
@ -469,10 +481,10 @@ export type Signin = {
}; };
export type UserSorting = export type UserSorting =
| '+follower' | "+follower"
| '-follower' | "-follower"
| '+createdAt' | "+createdAt"
| '-createdAt' | "-createdAt"
| '+updatedAt' | "+updatedAt"
| '-updatedAt'; | "-updatedAt";
export type OriginType = 'combined' | 'local' | 'remote'; export type OriginType = "combined" | "local" | "remote";

View File

@ -1,16 +1,10 @@
import { Endpoints } from './api.types'; import { Endpoints } from "./api.types";
import Stream, { Connection } from './streaming'; import Stream, { Connection } from "./streaming";
import { Channels } from './streaming.types'; import { Channels } from "./streaming.types";
import { Acct } from './acct'; import { Acct } from "./acct";
import * as consts from './consts'; import * as consts from "./consts";
export { export { Endpoints, Stream, Connection as ChannelConnection, Channels, Acct };
Endpoints,
Stream,
Connection as ChannelConnection,
Channels,
Acct,
};
export const permissions = consts.permissions; export const permissions = consts.permissions;
export const notificationTypes = consts.notificationTypes; export const notificationTypes = consts.notificationTypes;
@ -21,6 +15,6 @@ export const ffVisibility = consts.ffVisibility;
// api extractor not supported yet // api extractor not supported yet
//export * as api from './api'; //export * as api from './api';
//export * as entities from './entities'; //export * as entities from './entities';
import * as api from './api'; import * as api from "./api";
import * as entities from './entities'; import * as entities from "./entities";
export { api, entities }; export { api, entities };

View File

@ -1,17 +1,22 @@
import autobind from 'autobind-decorator'; import autobind from "autobind-decorator";
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from "eventemitter3";
import ReconnectingWebsocket from 'reconnecting-websocket'; import ReconnectingWebsocket from "reconnecting-websocket";
import { BroadcastEvents, Channels } from './streaming.types'; import { BroadcastEvents, Channels } from "./streaming.types";
export function urlQuery(obj: Record<string, string | number | boolean | undefined>): string { export function urlQuery(
obj: Record<string, string | number | boolean | undefined>,
): string {
const params = Object.entries(obj) const params = Object.entries(obj)
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) .filter(([, v]) => (Array.isArray(v) ? v.length : v !== undefined))
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.reduce((a, [k, v]) => (a[k] = v!, a), {} as Record<string, string | number | boolean>); .reduce(
(a, [k, v]) => ((a[k] = v!), a),
{} as Record<string, string | number | boolean>,
);
return Object.entries(params) return Object.entries(params)
.map((e) => `${e[0]}=${encodeURIComponent(e[1])}`) .map((e) => `${e[0]}=${encodeURIComponent(e[1])}`)
.join('&'); .join("&");
} }
type AnyOf<T extends Record<any, any>> = T[keyof T]; type AnyOf<T extends Record<any, any>> = T[keyof T];
@ -26,17 +31,21 @@ type StreamEvents = {
*/ */
export default class Stream extends EventEmitter<StreamEvents> { export default class Stream extends EventEmitter<StreamEvents> {
private stream: ReconnectingWebsocket; private stream: ReconnectingWebsocket;
public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing'; public state: "initializing" | "reconnecting" | "connected" = "initializing";
private sharedConnectionPools: Pool[] = []; private sharedConnectionPools: Pool[] = [];
private sharedConnections: SharedConnection[] = []; private sharedConnections: SharedConnection[] = [];
private nonSharedConnections: NonSharedConnection[] = []; private nonSharedConnections: NonSharedConnection[] = [];
private idCounter = 0; private idCounter = 0;
constructor(origin: string, user: { token: string; } | null, options?: { constructor(
WebSocket?: any; origin: string,
}) { user: { token: string } | null,
options?: {
WebSocket?: any;
},
) {
super(); super();
options = options || { }; options = options || {};
const query = urlQuery({ const query = urlQuery({
i: user?.token, i: user?.token,
@ -45,15 +54,21 @@ export default class Stream extends EventEmitter<StreamEvents> {
_t: Date.now(), _t: Date.now(),
}); });
const wsOrigin = origin.replace('http://', 'ws://').replace('https://', 'wss://'); const wsOrigin = origin
.replace("http://", "ws://")
.replace("https://", "wss://");
this.stream = new ReconnectingWebsocket(`${wsOrigin}/streaming?${query}`, '', { this.stream = new ReconnectingWebsocket(
minReconnectionDelay: 1, // https://github.com/pladaria/reconnecting-websocket/issues/91 `${wsOrigin}/streaming?${query}`,
WebSocket: options.WebSocket, "",
}); {
this.stream.addEventListener('open', this.onOpen); minReconnectionDelay: 1, // https://github.com/pladaria/reconnecting-websocket/issues/91
this.stream.addEventListener('close', this.onClose); WebSocket: options.WebSocket,
this.stream.addEventListener('message', this.onMessage); },
);
this.stream.addEventListener("open", this.onOpen);
this.stream.addEventListener("close", this.onClose);
this.stream.addEventListener("message", this.onMessage);
} }
@autobind @autobind
@ -62,7 +77,11 @@ export default class Stream extends EventEmitter<StreamEvents> {
} }
@autobind @autobind
public useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): Connection<Channels[C]> { public useChannel<C extends keyof Channels>(
channel: C,
params?: Channels[C]["params"],
name?: string,
): Connection<Channels[C]> {
if (params) { if (params) {
return this.connectToChannel(channel, params); return this.connectToChannel(channel, params);
} else { } else {
@ -71,8 +90,11 @@ export default class Stream extends EventEmitter<StreamEvents> {
} }
@autobind @autobind
private useSharedConnection<C extends keyof Channels>(channel: C, name?: string): SharedConnection<Channels[C]> { private useSharedConnection<C extends keyof Channels>(
let pool = this.sharedConnectionPools.find(p => p.channel === channel); channel: C,
name?: string,
): SharedConnection<Channels[C]> {
let pool = this.sharedConnectionPools.find((p) => p.channel === channel);
if (pool == null) { if (pool == null) {
pool = new Pool(this, channel, this.genId()); pool = new Pool(this, channel, this.genId());
@ -86,24 +108,38 @@ export default class Stream extends EventEmitter<StreamEvents> {
@autobind @autobind
public removeSharedConnection(connection: SharedConnection): void { public removeSharedConnection(connection: SharedConnection): void {
this.sharedConnections = this.sharedConnections.filter(c => c !== connection); this.sharedConnections = this.sharedConnections.filter(
(c) => c !== connection,
);
} }
@autobind @autobind
public removeSharedConnectionPool(pool: Pool): void { public removeSharedConnectionPool(pool: Pool): void {
this.sharedConnectionPools = this.sharedConnectionPools.filter(p => p !== pool); this.sharedConnectionPools = this.sharedConnectionPools.filter(
(p) => p !== pool,
);
} }
@autobind @autobind
private connectToChannel<C extends keyof Channels>(channel: C, params: Channels[C]['params']): NonSharedConnection<Channels[C]> { private connectToChannel<C extends keyof Channels>(
const connection = new NonSharedConnection(this, channel, this.genId(), params); channel: C,
params: Channels[C]["params"],
): NonSharedConnection<Channels[C]> {
const connection = new NonSharedConnection(
this,
channel,
this.genId(),
params,
);
this.nonSharedConnections.push(connection); this.nonSharedConnections.push(connection);
return connection; return connection;
} }
@autobind @autobind
public disconnectToChannel(connection: NonSharedConnection): void { public disconnectToChannel(connection: NonSharedConnection): void {
this.nonSharedConnections = this.nonSharedConnections.filter(c => c !== connection); this.nonSharedConnections = this.nonSharedConnections.filter(
(c) => c !== connection,
);
} }
/** /**
@ -111,10 +147,10 @@ export default class Stream extends EventEmitter<StreamEvents> {
*/ */
@autobind @autobind
private onOpen(): void { private onOpen(): void {
const isReconnect = this.state === 'reconnecting'; const isReconnect = this.state === "reconnecting";
this.state = 'connected'; this.state = "connected";
this.emit('_connected_'); this.emit("_connected_");
// チャンネル再接続 // チャンネル再接続
if (isReconnect) { if (isReconnect) {
@ -128,9 +164,9 @@ export default class Stream extends EventEmitter<StreamEvents> {
*/ */
@autobind @autobind
private onClose(): void { private onClose(): void {
if (this.state === 'connected') { if (this.state === "connected") {
this.state = 'reconnecting'; this.state = "reconnecting";
this.emit('_disconnected_'); this.emit("_disconnected_");
} }
} }
@ -138,18 +174,18 @@ export default class Stream extends EventEmitter<StreamEvents> {
* Callback of when received a message from connection * Callback of when received a message from connection
*/ */
@autobind @autobind
private onMessage(message: { data: string; }): void { private onMessage(message: { data: string }): void {
const { type, body } = JSON.parse(message.data); const { type, body } = JSON.parse(message.data);
if (type === 'channel') { if (type === "channel") {
const id = body.id; const id = body.id;
let connections: Connection[]; let connections: Connection[];
connections = this.sharedConnections.filter(c => c.id === id); connections = this.sharedConnections.filter((c) => c.id === id);
if (connections.length === 0) { if (connections.length === 0) {
const found = this.nonSharedConnections.find(c => c.id === id); const found = this.nonSharedConnections.find((c) => c.id === id);
if (found) { if (found) {
connections = [found]; connections = [found];
} }
@ -169,10 +205,13 @@ export default class Stream extends EventEmitter<StreamEvents> {
*/ */
@autobind @autobind
public send(typeOrPayload: any, payload?: any): void { public send(typeOrPayload: any, payload?: any): void {
const data = payload === undefined ? typeOrPayload : { const data =
type: typeOrPayload, payload === undefined
body: payload, ? typeOrPayload
}; : {
type: typeOrPayload,
body: payload,
};
this.stream.send(JSON.stringify(data)); this.stream.send(JSON.stringify(data));
} }
@ -201,7 +240,7 @@ class Pool {
this.stream = stream; this.stream = stream;
this.id = id; this.id = id;
this.stream.on('_disconnected_', this.onStreamDisconnected); this.stream.on("_disconnected_", this.onStreamDisconnected);
} }
@autobind @autobind
@ -242,7 +281,7 @@ class Pool {
public connect(): void { public connect(): void {
if (this.isConnected) return; if (this.isConnected) return;
this.isConnected = true; this.isConnected = true;
this.stream.send('connect', { this.stream.send("connect", {
channel: this.channel, channel: this.channel,
id: this.id, id: this.id,
}); });
@ -250,13 +289,15 @@ class Pool {
@autobind @autobind
private disconnect(): void { private disconnect(): void {
this.stream.off('_disconnected_', this.onStreamDisconnected); this.stream.off("_disconnected_", this.onStreamDisconnected);
this.stream.send('disconnect', { id: this.id }); this.stream.send("disconnect", { id: this.id });
this.stream.removeSharedConnectionPool(this); this.stream.removeSharedConnectionPool(this);
} }
} }
export abstract class Connection<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> { export abstract class Connection<
Channel extends AnyOf<Channels> = any,
> extends EventEmitter<Channel["events"]> {
public channel: string; public channel: string;
protected stream: Stream; protected stream: Stream;
public abstract id: string; public abstract id: string;
@ -274,8 +315,11 @@ export abstract class Connection<Channel extends AnyOf<Channels> = any> extends
} }
@autobind @autobind
public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void { public send<T extends keyof Channel["receives"]>(
this.stream.send('ch', { type: T,
body: Channel["receives"][T],
): void {
this.stream.send("ch", {
id: this.id, id: this.id,
type: type, type: type,
body: body, body: body,
@ -287,7 +331,9 @@ export abstract class Connection<Channel extends AnyOf<Channels> = any> extends
public abstract dispose(): void; public abstract dispose(): void;
} }
class SharedConnection<Channel extends AnyOf<Channels> = any> extends Connection<Channel> { class SharedConnection<
Channel extends AnyOf<Channels> = any,
> extends Connection<Channel> {
private pool: Pool; private pool: Pool;
public get id(): string { public get id(): string {
@ -309,11 +355,18 @@ class SharedConnection<Channel extends AnyOf<Channels> = any> extends Connection
} }
} }
class NonSharedConnection<Channel extends AnyOf<Channels> = any> extends Connection<Channel> { class NonSharedConnection<
Channel extends AnyOf<Channels> = any,
> extends Connection<Channel> {
public id: string; public id: string;
protected params: Channel['params']; protected params: Channel["params"];
constructor(stream: Stream, channel: string, id: string, params: Channel['params']) { constructor(
stream: Stream,
channel: string,
id: string,
params: Channel["params"],
) {
super(stream, channel); super(stream, channel);
this.params = params; this.params = params;
@ -324,7 +377,7 @@ class NonSharedConnection<Channel extends AnyOf<Channels> = any> extends Connect
@autobind @autobind
public connect(): void { public connect(): void {
this.stream.send('connect', { this.stream.send("connect", {
channel: this.channel, channel: this.channel,
id: this.id, id: this.id,
params: this.params, params: this.params,
@ -334,7 +387,7 @@ class NonSharedConnection<Channel extends AnyOf<Channels> = any> extends Connect
@autobind @autobind
public dispose(): void { public dispose(): void {
this.removeAllListeners(); this.removeAllListeners();
this.stream.send('disconnect', { id: this.id }); this.stream.send("disconnect", { id: this.id });
this.stream.disconnectToChannel(this); this.stream.disconnectToChannel(this);
} }
} }

View File

@ -1,4 +1,15 @@
import { Antenna, CustomEmoji, DriveFile, MeDetailed, MessagingMessage, Note, Notification, PageEvent, User, UserGroup } from './entities'; import {
Antenna,
CustomEmoji,
DriveFile,
MeDetailed,
MessagingMessage,
Note,
Notification,
PageEvent,
User,
UserGroup,
} from "./entities";
type FIXME = any; type FIXME = any;
@ -15,12 +26,12 @@ export type Channels = {
unfollow: (payload: User) => void; // 自分が他人をフォロー解除したとき unfollow: (payload: User) => void; // 自分が他人をフォロー解除したとき
meUpdated: (payload: MeDetailed) => void; meUpdated: (payload: MeDetailed) => void;
pageEvent: (payload: PageEvent) => void; pageEvent: (payload: PageEvent) => void;
urlUploadFinished: (payload: { marker: string; file: DriveFile; }) => void; urlUploadFinished: (payload: { marker: string; file: DriveFile }) => void;
readAllNotifications: () => void; readAllNotifications: () => void;
unreadNotification: (payload: Notification) => void; unreadNotification: (payload: Notification) => void;
unreadMention: (payload: Note['id']) => void; unreadMention: (payload: Note["id"]) => void;
readAllUnreadMentions: () => void; readAllUnreadMentions: () => void;
unreadSpecifiedNote: (payload: Note['id']) => void; unreadSpecifiedNote: (payload: Note["id"]) => void;
readAllUnreadSpecifiedNotes: () => void; readAllUnreadSpecifiedNotes: () => void;
readAllMessagingMessages: () => void; readAllMessagingMessages: () => void;
messagingMessage: (payload: MessagingMessage) => void; messagingMessage: (payload: MessagingMessage) => void;
@ -29,7 +40,7 @@ export type Channels = {
unreadAntenna: (payload: Antenna) => void; unreadAntenna: (payload: Antenna) => void;
readAllAnnouncements: () => void; readAllAnnouncements: () => void;
readAllChannels: () => void; readAllChannels: () => void;
unreadChannel: (payload: Note['id']) => void; unreadChannel: (payload: Note["id"]) => void;
myTokenRegenerated: () => void; myTokenRegenerated: () => void;
reversiNoInvites: () => void; reversiNoInvites: () => void;
reversiInvited: (payload: FIXME) => void; reversiInvited: (payload: FIXME) => void;
@ -81,18 +92,18 @@ export type Channels = {
}; };
messaging: { messaging: {
params: { params: {
otherparty?: User['id'] | null; otherparty?: User["id"] | null;
group?: UserGroup['id'] | null; group?: UserGroup["id"] | null;
}; };
events: { events: {
message: (payload: MessagingMessage) => void; message: (payload: MessagingMessage) => void;
deleted: (payload: MessagingMessage['id']) => void; deleted: (payload: MessagingMessage["id"]) => void;
read: (payload: MessagingMessage['id'][]) => void; read: (payload: MessagingMessage["id"][]) => void;
typers: (payload: User[]) => void; typers: (payload: User[]) => void;
}; };
receives: { receives: {
read: { read: {
id: MessagingMessage['id']; id: MessagingMessage["id"];
}; };
}; };
}; };
@ -122,40 +133,45 @@ export type Channels = {
}; };
}; };
export type NoteUpdatedEvent = { export type NoteUpdatedEvent =
id: Note['id']; | {
type: 'reacted'; id: Note["id"];
body: { type: "reacted";
reaction: string; body: {
userId: User['id']; reaction: string;
}; userId: User["id"];
} | { };
id: Note['id']; }
type: 'unreacted'; | {
body: { id: Note["id"];
reaction: string; type: "unreacted";
userId: User['id']; body: {
}; reaction: string;
} | { userId: User["id"];
id: Note['id']; };
type: 'deleted'; }
body: { | {
deletedAt: string; id: Note["id"];
}; type: "deleted";
} | { body: {
id: Note['id']; deletedAt: string;
type: 'pollVoted'; };
body: { }
choice: number; | {
userId: User['id']; id: Note["id"];
}; type: "pollVoted";
} | { body: {
id: Note['id']; choice: number;
type: 'replied'; userId: User["id"];
body: { };
id: Note['id']; }
}; | {
}; id: Note["id"];
type: "replied";
body: {
id: Note["id"];
};
};
export type BroadcastEvents = { export type BroadcastEvents = {
noteUpdated: (payload: NoteUpdatedEvent) => void; noteUpdated: (payload: NoteUpdatedEvent) => void;

View File

@ -1,45 +1,48 @@
import { expectType } from 'tsd'; import { expectType } from "tsd";
import * as Misskey from '../src'; import * as Misskey from "../src";
describe('API', () => { describe("API", () => {
test('success', async () => { test("success", async () => {
const cli = new Misskey.api.APIClient({ const cli = new Misskey.api.APIClient({
origin: 'https://misskey.test', origin: "https://misskey.test",
credential: 'TOKEN' credential: "TOKEN",
}); });
const res = await cli.request('meta', { detail: true }); const res = await cli.request("meta", { detail: true });
expectType<Misskey.entities.DetailedInstanceMetadata>(res); expectType<Misskey.entities.DetailedInstanceMetadata>(res);
}); });
test('conditional respose type (meta)', async () => { test("conditional respose type (meta)", async () => {
const cli = new Misskey.api.APIClient({ const cli = new Misskey.api.APIClient({
origin: 'https://misskey.test', origin: "https://misskey.test",
credential: 'TOKEN' credential: "TOKEN",
}); });
const res = await cli.request('meta', { detail: true }); const res = await cli.request("meta", { detail: true });
expectType<Misskey.entities.DetailedInstanceMetadata>(res); expectType<Misskey.entities.DetailedInstanceMetadata>(res);
const res2 = await cli.request('meta', { detail: false }); const res2 = await cli.request("meta", { detail: false });
expectType<Misskey.entities.LiteInstanceMetadata>(res2); expectType<Misskey.entities.LiteInstanceMetadata>(res2);
const res3 = await cli.request('meta', { }); const res3 = await cli.request("meta", {});
expectType<Misskey.entities.LiteInstanceMetadata>(res3); expectType<Misskey.entities.LiteInstanceMetadata>(res3);
const res4 = await cli.request('meta', { detail: true as boolean }); const res4 = await cli.request("meta", { detail: true as boolean });
expectType<Misskey.entities.LiteInstanceMetadata | Misskey.entities.DetailedInstanceMetadata>(res4); expectType<
| Misskey.entities.LiteInstanceMetadata
| Misskey.entities.DetailedInstanceMetadata
>(res4);
}); });
test('conditional respose type (users/show)', async () => { test("conditional respose type (users/show)", async () => {
const cli = new Misskey.api.APIClient({ const cli = new Misskey.api.APIClient({
origin: 'https://misskey.test', origin: "https://misskey.test",
credential: 'TOKEN' credential: "TOKEN",
}); });
const res = await cli.request('users/show', { userId: 'xxxxxxxx' }); const res = await cli.request("users/show", { userId: "xxxxxxxx" });
expectType<Misskey.entities.UserDetailed>(res); expectType<Misskey.entities.UserDetailed>(res);
const res2 = await cli.request('users/show', { userIds: ['xxxxxxxx'] }); const res2 = await cli.request("users/show", { userIds: ["xxxxxxxx"] });
expectType<Misskey.entities.UserDetailed[]>(res2); expectType<Misskey.entities.UserDetailed[]>(res2);
}); });
}); });

View File

@ -1,25 +1,31 @@
import { expectType } from 'tsd'; import { expectType } from "tsd";
import * as Misskey from '../src'; import * as Misskey from "../src";
describe('Streaming', () => { describe("Streaming", () => {
test('emit type', async () => { test("emit type", async () => {
const stream = new Misskey.Stream('https://misskey.test', { token: 'TOKEN' }); const stream = new Misskey.Stream("https://misskey.test", {
const mainChannel = stream.useChannel('main'); token: "TOKEN",
mainChannel.on('notification', notification => { });
const mainChannel = stream.useChannel("main");
mainChannel.on("notification", (notification) => {
expectType<Misskey.entities.Notification>(notification); expectType<Misskey.entities.Notification>(notification);
}); });
}); });
test('params type', async () => { test("params type", async () => {
const stream = new Misskey.Stream('https://misskey.test', { token: 'TOKEN' }); const stream = new Misskey.Stream("https://misskey.test", {
token: "TOKEN",
});
// TODO: 「stream.useChannel の第二引数として受け入れる型が // TODO: 「stream.useChannel の第二引数として受け入れる型が
// { // {
// otherparty?: User['id'] | null; // otherparty?: User['id'] | null;
// group?: UserGroup['id'] | null; // group?: UserGroup['id'] | null;
// } // }
// になっている」というテストを行いたいけどtsdでの書き方がわからない // になっている」というテストを行いたいけどtsdでの書き方がわからない
const messagingChannel = stream.useChannel('messaging', { otherparty: 'aaa' }); const messagingChannel = stream.useChannel("messaging", {
messagingChannel.on('message', message => { otherparty: "aaa",
});
messagingChannel.on("message", (message) => {
expectType<Misskey.entities.MessagingMessage>(message); expectType<Misskey.entities.MessagingMessage>(message);
}); });
}); });

View File

@ -1,28 +1,28 @@
import { APIClient, isAPIError } from '../src/api'; import { APIClient, isAPIError } from "../src/api";
import { enableFetchMocks } from 'jest-fetch-mock'; import { enableFetchMocks } from "jest-fetch-mock";
enableFetchMocks(); enableFetchMocks();
function getFetchCall(call: any[]) { function getFetchCall(call: any[]) {
const { body, method } = call[1]; const { body, method } = call[1];
if (body != null && typeof body != 'string') { if (body != null && typeof body != "string") {
throw new Error('invalid body'); throw new Error("invalid body");
} }
return { return {
url: call[0], url: call[0],
method: method, method: method,
body: JSON.parse(body as any) body: JSON.parse(body as any),
}; };
} }
describe('API', () => { describe("API", () => {
test('success', async () => { test("success", async () => {
fetchMock.resetMocks(); fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => { fetchMock.mockResponse(async (req) => {
const body = await req.json(); const body = await req.json();
if (req.method == 'POST' && req.url == 'https://misskey.test/api/i') { if (req.method == "POST" && req.url == "https://misskey.test/api/i") {
if (body.i === 'TOKEN') { if (body.i === "TOKEN") {
return JSON.stringify({ id: 'foo' }); return JSON.stringify({ id: "foo" });
} else { } else {
return { status: 400 }; return { status: 400 };
} }
@ -32,30 +32,33 @@ describe('API', () => {
}); });
const cli = new APIClient({ const cli = new APIClient({
origin: 'https://misskey.test', origin: "https://misskey.test",
credential: 'TOKEN', credential: "TOKEN",
}); });
const res = await cli.request('i'); const res = await cli.request("i");
expect(res).toEqual({ expect(res).toEqual({
id: 'foo' id: "foo",
}); });
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({ expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://misskey.test/api/i', url: "https://misskey.test/api/i",
method: 'POST', method: "POST",
body: { i: 'TOKEN' } body: { i: "TOKEN" },
}); });
}); });
test('with params', async () => { test("with params", async () => {
fetchMock.resetMocks(); fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => { fetchMock.mockResponse(async (req) => {
const body = await req.json(); const body = await req.json();
if (req.method == 'POST' && req.url == 'https://misskey.test/api/notes/show') { if (
if (body.i === 'TOKEN' && body.noteId === 'aaaaa') { req.method == "POST" &&
return JSON.stringify({ id: 'foo' }); req.url == "https://misskey.test/api/notes/show"
) {
if (body.i === "TOKEN" && body.noteId === "aaaaa") {
return JSON.stringify({ id: "foo" });
} else { } else {
return { status: 400 }; return { status: 400 };
} }
@ -65,27 +68,30 @@ describe('API', () => {
}); });
const cli = new APIClient({ const cli = new APIClient({
origin: 'https://misskey.test', origin: "https://misskey.test",
credential: 'TOKEN', credential: "TOKEN",
}); });
const res = await cli.request('notes/show', { noteId: 'aaaaa' }); const res = await cli.request("notes/show", { noteId: "aaaaa" });
expect(res).toEqual({ expect(res).toEqual({
id: 'foo' id: "foo",
}); });
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({ expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://misskey.test/api/notes/show', url: "https://misskey.test/api/notes/show",
method: 'POST', method: "POST",
body: { i: 'TOKEN', noteId: 'aaaaa' } body: { i: "TOKEN", noteId: "aaaaa" },
}); });
}); });
test('204 No Content で null が返る', async () => { test("204 No Content で null が返る", async () => {
fetchMock.resetMocks(); fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => { fetchMock.mockResponse(async (req) => {
if (req.method == 'POST' && req.url == 'https://misskey.test/api/reset-password') { if (
req.method == "POST" &&
req.url == "https://misskey.test/api/reset-password"
) {
return { status: 204 }; return { status: 204 };
} else { } else {
return { status: 404 }; return { status: 404 };
@ -93,38 +99,41 @@ describe('API', () => {
}); });
const cli = new APIClient({ const cli = new APIClient({
origin: 'https://misskey.test', origin: "https://misskey.test",
credential: 'TOKEN', credential: "TOKEN",
}); });
const res = await cli.request('reset-password', { token: 'aaa', password: 'aaa' }); const res = await cli.request("reset-password", {
token: "aaa",
password: "aaa",
});
expect(res).toEqual(null); expect(res).toEqual(null);
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({ expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://misskey.test/api/reset-password', url: "https://misskey.test/api/reset-password",
method: 'POST', method: "POST",
body: { i: 'TOKEN', token: 'aaa', password: 'aaa' } body: { i: "TOKEN", token: "aaa", password: "aaa" },
}); });
}); });
test('インスタンスの credential が指定されていても引数で credential が null ならば null としてリクエストされる', async () => { test("インスタンスの credential が指定されていても引数で credential が null ならば null としてリクエストされる", async () => {
fetchMock.resetMocks(); fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => { fetchMock.mockResponse(async (req) => {
const body = await req.json(); const body = await req.json();
if (req.method == 'POST' && req.url == 'https://misskey.test/api/i') { if (req.method == "POST" && req.url == "https://misskey.test/api/i") {
if (typeof body.i === 'string') { if (typeof body.i === "string") {
return JSON.stringify({ id: 'foo' }); return JSON.stringify({ id: "foo" });
} else { } else {
return { return {
status: 401, status: 401,
body: JSON.stringify({ body: JSON.stringify({
error: { error: {
message: 'Credential required.', message: "Credential required.",
code: 'CREDENTIAL_REQUIRED', code: "CREDENTIAL_REQUIRED",
id: '1384574d-a912-4b81-8601-c7b1c4085df1', id: "1384574d-a912-4b81-8601-c7b1c4085df1",
} },
}) }),
}; };
} }
} else { } else {
@ -134,77 +143,78 @@ describe('API', () => {
try { try {
const cli = new APIClient({ const cli = new APIClient({
origin: 'https://misskey.test', origin: "https://misskey.test",
credential: 'TOKEN', credential: "TOKEN",
}); });
await cli.request('i', {}, null); await cli.request("i", {}, null);
} catch (e) { } catch (e) {
expect(isAPIError(e)).toEqual(true); expect(isAPIError(e)).toEqual(true);
} }
}); });
test('api error', async () => { test("api error", async () => {
fetchMock.resetMocks(); fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => { fetchMock.mockResponse(async (req) => {
return { return {
status: 500, status: 500,
body: JSON.stringify({ body: JSON.stringify({
error: { error: {
message: 'Internal error occurred. Please contact us if the error persists.', message:
code: 'INTERNAL_ERROR', "Internal error occurred. Please contact us if the error persists.",
id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac', code: "INTERNAL_ERROR",
kind: 'server', id: "5d37dbcb-891e-41ca-a3d6-e690c97775ac",
kind: "server",
}, },
}) }),
}; };
}); });
try { try {
const cli = new APIClient({ const cli = new APIClient({
origin: 'https://misskey.test', origin: "https://misskey.test",
credential: 'TOKEN', credential: "TOKEN",
}); });
await cli.request('i'); await cli.request("i");
} catch (e: any) { } catch (e: any) {
expect(isAPIError(e)).toEqual(true); expect(isAPIError(e)).toEqual(true);
expect(e.id).toEqual('5d37dbcb-891e-41ca-a3d6-e690c97775ac'); expect(e.id).toEqual("5d37dbcb-891e-41ca-a3d6-e690c97775ac");
} }
}); });
test('network error', async () => { test("network error", async () => {
fetchMock.resetMocks(); fetchMock.resetMocks();
fetchMock.mockAbort(); fetchMock.mockAbort();
try { try {
const cli = new APIClient({ const cli = new APIClient({
origin: 'https://misskey.test', origin: "https://misskey.test",
credential: 'TOKEN', credential: "TOKEN",
}); });
await cli.request('i'); await cli.request("i");
} catch (e) { } catch (e) {
expect(isAPIError(e)).toEqual(false); expect(isAPIError(e)).toEqual(false);
} }
}); });
test('json parse error', async () => { test("json parse error", async () => {
fetchMock.resetMocks(); fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => { fetchMock.mockResponse(async (req) => {
return { return {
status: 500, status: 500,
body: '<html>I AM NOT JSON</html>' body: "<html>I AM NOT JSON</html>",
}; };
}); });
try { try {
const cli = new APIClient({ const cli = new APIClient({
origin: 'https://misskey.test', origin: "https://misskey.test",
credential: 'TOKEN', credential: "TOKEN",
}); });
await cli.request('i'); await cli.request("i");
} catch (e) { } catch (e) {
expect(isAPIError(e)).toEqual(false); expect(isAPIError(e)).toEqual(false);
} }

View File

@ -1,95 +1,105 @@
import WS from 'jest-websocket-mock'; import WS from "jest-websocket-mock";
import Stream from '../src/streaming'; import Stream from "../src/streaming";
describe('Streaming', () => { describe("Streaming", () => {
test('useChannel', async () => { test("useChannel", async () => {
const server = new WS('wss://misskey.test/streaming'); const server = new WS("wss://misskey.test/streaming");
const stream = new Stream('https://misskey.test', { token: 'TOKEN' }); const stream = new Stream("https://misskey.test", { token: "TOKEN" });
const mainChannelReceived: any[] = []; const mainChannelReceived: any[] = [];
const main = stream.useChannel('main'); const main = stream.useChannel("main");
main.on('meUpdated', payload => { main.on("meUpdated", (payload) => {
mainChannelReceived.push(payload); mainChannelReceived.push(payload);
}); });
const ws = await server.connected; const ws = await server.connected;
expect(new URLSearchParams(new URL(ws.url).search).get('i')).toEqual('TOKEN'); expect(new URLSearchParams(new URL(ws.url).search).get("i")).toEqual(
"TOKEN",
);
const msg = JSON.parse(await server.nextMessage as string); const msg = JSON.parse((await server.nextMessage) as string);
const mainChannelId = msg.body.id; const mainChannelId = msg.body.id;
expect(msg.type).toEqual('connect'); expect(msg.type).toEqual("connect");
expect(msg.body.channel).toEqual('main'); expect(msg.body.channel).toEqual("main");
expect(mainChannelId != null).toEqual(true); expect(mainChannelId != null).toEqual(true);
server.send(JSON.stringify({ server.send(
type: 'channel', JSON.stringify({
body: { type: "channel",
id: mainChannelId,
type: 'meUpdated',
body: { body: {
id: 'foo' id: mainChannelId,
} type: "meUpdated",
} body: {
})); id: "foo",
},
},
}),
);
expect(mainChannelReceived[0]).toEqual({ expect(mainChannelReceived[0]).toEqual({
id: 'foo' id: "foo",
}); });
stream.close(); stream.close();
server.close(); server.close();
}); });
test('useChannel with parameters', async () => { test("useChannel with parameters", async () => {
const server = new WS('wss://misskey.test/streaming'); const server = new WS("wss://misskey.test/streaming");
const stream = new Stream('https://misskey.test', { token: 'TOKEN' }); const stream = new Stream("https://misskey.test", { token: "TOKEN" });
const messagingChannelReceived: any[] = []; const messagingChannelReceived: any[] = [];
const messaging = stream.useChannel('messaging', { otherparty: 'aaa' }); const messaging = stream.useChannel("messaging", { otherparty: "aaa" });
messaging.on('message', payload => { messaging.on("message", (payload) => {
messagingChannelReceived.push(payload); messagingChannelReceived.push(payload);
}); });
const ws = await server.connected; const ws = await server.connected;
expect(new URLSearchParams(new URL(ws.url).search).get('i')).toEqual('TOKEN'); expect(new URLSearchParams(new URL(ws.url).search).get("i")).toEqual(
"TOKEN",
);
const msg = JSON.parse(await server.nextMessage as string); const msg = JSON.parse((await server.nextMessage) as string);
const messagingChannelId = msg.body.id; const messagingChannelId = msg.body.id;
expect(msg.type).toEqual('connect'); expect(msg.type).toEqual("connect");
expect(msg.body.channel).toEqual('messaging'); expect(msg.body.channel).toEqual("messaging");
expect(msg.body.params).toEqual({ otherparty: 'aaa' }); expect(msg.body.params).toEqual({ otherparty: "aaa" });
expect(messagingChannelId != null).toEqual(true); expect(messagingChannelId != null).toEqual(true);
server.send(JSON.stringify({ server.send(
type: 'channel', JSON.stringify({
body: { type: "channel",
id: messagingChannelId,
type: 'message',
body: { body: {
id: 'foo' id: messagingChannelId,
} type: "message",
} body: {
})); id: "foo",
},
},
}),
);
expect(messagingChannelReceived[0]).toEqual({ expect(messagingChannelReceived[0]).toEqual({
id: 'foo' id: "foo",
}); });
stream.close(); stream.close();
server.close(); server.close();
}); });
test('ちゃんとチャンネルごとにidが異なる', async () => { test("ちゃんとチャンネルごとにidが異なる", async () => {
const server = new WS('wss://misskey.test/streaming'); const server = new WS("wss://misskey.test/streaming");
const stream = new Stream('https://misskey.test', { token: 'TOKEN' }); const stream = new Stream("https://misskey.test", { token: "TOKEN" });
stream.useChannel('messaging', { otherparty: 'aaa' }); stream.useChannel("messaging", { otherparty: "aaa" });
stream.useChannel('messaging', { otherparty: 'bbb' }); stream.useChannel("messaging", { otherparty: "bbb" });
const ws = await server.connected; const ws = await server.connected;
expect(new URLSearchParams(new URL(ws.url).search).get('i')).toEqual('TOKEN'); expect(new URLSearchParams(new URL(ws.url).search).get("i")).toEqual(
"TOKEN",
);
const msg = JSON.parse(await server.nextMessage as string); const msg = JSON.parse((await server.nextMessage) as string);
const messagingChannelId = msg.body.id; const messagingChannelId = msg.body.id;
const msg2 = JSON.parse(await server.nextMessage as string); const msg2 = JSON.parse((await server.nextMessage) as string);
const messagingChannelId2 = msg2.body.id; const messagingChannelId2 = msg2.body.id;
expect(messagingChannelId != null).toEqual(true); expect(messagingChannelId != null).toEqual(true);
@ -100,58 +110,64 @@ describe('Streaming', () => {
server.close(); server.close();
}); });
test('Connection#send', async () => { test("Connection#send", async () => {
const server = new WS('wss://misskey.test/streaming'); const server = new WS("wss://misskey.test/streaming");
const stream = new Stream('https://misskey.test', { token: 'TOKEN' }); const stream = new Stream("https://misskey.test", { token: "TOKEN" });
const messaging = stream.useChannel('messaging', { otherparty: 'aaa' }); const messaging = stream.useChannel("messaging", { otherparty: "aaa" });
messaging.send('read', { id: 'aaa' }); messaging.send("read", { id: "aaa" });
const ws = await server.connected; const ws = await server.connected;
expect(new URLSearchParams(new URL(ws.url).search).get('i')).toEqual('TOKEN'); expect(new URLSearchParams(new URL(ws.url).search).get("i")).toEqual(
"TOKEN",
);
const connectMsg = JSON.parse(await server.nextMessage as string); const connectMsg = JSON.parse((await server.nextMessage) as string);
const channelId = connectMsg.body.id; const channelId = connectMsg.body.id;
const msg = JSON.parse(await server.nextMessage as string); const msg = JSON.parse((await server.nextMessage) as string);
expect(msg.type).toEqual('ch'); expect(msg.type).toEqual("ch");
expect(msg.body.id).toEqual(channelId); expect(msg.body.id).toEqual(channelId);
expect(msg.body.type).toEqual('read'); expect(msg.body.type).toEqual("read");
expect(msg.body.body).toEqual({ id: 'aaa' }); expect(msg.body.body).toEqual({ id: "aaa" });
stream.close(); stream.close();
server.close(); server.close();
}); });
test('Connection#dispose', async () => { test("Connection#dispose", async () => {
const server = new WS('wss://misskey.test/streaming'); const server = new WS("wss://misskey.test/streaming");
const stream = new Stream('https://misskey.test', { token: 'TOKEN' }); const stream = new Stream("https://misskey.test", { token: "TOKEN" });
const mainChannelReceived: any[] = []; const mainChannelReceived: any[] = [];
const main = stream.useChannel('main'); const main = stream.useChannel("main");
main.on('meUpdated', payload => { main.on("meUpdated", (payload) => {
mainChannelReceived.push(payload); mainChannelReceived.push(payload);
}); });
const ws = await server.connected;
expect(new URLSearchParams(new URL(ws.url).search).get('i')).toEqual('TOKEN');
const msg = JSON.parse(await server.nextMessage as string); const ws = await server.connected;
expect(new URLSearchParams(new URL(ws.url).search).get("i")).toEqual(
"TOKEN",
);
const msg = JSON.parse((await server.nextMessage) as string);
const mainChannelId = msg.body.id; const mainChannelId = msg.body.id;
expect(msg.type).toEqual('connect'); expect(msg.type).toEqual("connect");
expect(msg.body.channel).toEqual('main'); expect(msg.body.channel).toEqual("main");
expect(mainChannelId != null).toEqual(true); expect(mainChannelId != null).toEqual(true);
main.dispose(); main.dispose();
server.send(JSON.stringify({ server.send(
type: 'channel', JSON.stringify({
body: { type: "channel",
id: mainChannelId,
type: 'meUpdated',
body: { body: {
id: 'foo' id: mainChannelId,
} type: "meUpdated",
} body: {
})); id: "foo",
},
},
}),
);
expect(mainChannelReceived.length).toEqual(0); expect(mainChannelReceived.length).toEqual(0);

View File

@ -1,7 +1,7 @@
import { markRaw, ref } from "vue"; import { markRaw, ref } from "vue";
import { Storage } from "./pizzax"; import { Storage } from "./pizzax";
import { Theme } from "./scripts/theme"; import { Theme } from "./scripts/theme";
import { deviceKind } from '@/scripts/device-kind'; import { deviceKind } from "@/scripts/device-kind";
export const postFormActions = []; export const postFormActions = [];
export const userActions = []; export const userActions = [];
@ -16,10 +16,10 @@ const menuOptions = [
"explore", "explore",
"favorites", "favorites",
"channels", "channels",
"search" "search",
]; ];
if (deviceKind === 'desktop') menuOptions.push("ui"); if (deviceKind === "desktop") menuOptions.push("ui");
// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう) // TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
// あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない // あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない