From 6a878a1194f4ed5508b43be259dba8d7b689c566 Mon Sep 17 00:00:00 2001 From: Esurio Date: Sun, 25 Aug 2024 14:41:43 +0000 Subject: [PATCH] run format --- packages/megalodon/src/mastodon/api_client.ts | 1399 +++++++++-------- .../megalodon/src/mastodon/notification.ts | 25 +- packages/megalodon/src/mastodon/web_socket.ts | 667 ++++---- 3 files changed, 1125 insertions(+), 966 deletions(-) diff --git a/packages/megalodon/src/mastodon/api_client.ts b/packages/megalodon/src/mastodon/api_client.ts index f0fc2ea854..f07974464d 100644 --- a/packages/megalodon/src/mastodon/api_client.ts +++ b/packages/megalodon/src/mastodon/api_client.ts @@ -1,661 +1,780 @@ -import axios, { type AxiosResponse, type AxiosRequestConfig } from 'axios' -import objectAssignDeep from 'object-assign-deep' +import axios, { type AxiosResponse, type AxiosRequestConfig } from "axios"; +import objectAssignDeep from "object-assign-deep"; -import WebSocket from './web_socket' -import type Response from '../response' -import { RequestCanceledError } from '../cancel' -import proxyAgent, { type ProxyConfig } from '../proxy_config' -import { NO_REDIRECT, DEFAULT_SCOPE, DEFAULT_UA } from '../default' -import type MastodonEntity from './entity' -import type MegalodonEntity from '../entity' -import NotificationType, { UnknownNotificationTypeError } from '../notification' -import MastodonNotificationType from './notification' +import WebSocket from "./web_socket"; +import type Response from "../response"; +import { RequestCanceledError } from "../cancel"; +import proxyAgent, { type ProxyConfig } from "../proxy_config"; +import { NO_REDIRECT, DEFAULT_SCOPE, DEFAULT_UA } from "../default"; +import type MastodonEntity from "./entity"; +import type MegalodonEntity from "../entity"; +import NotificationType, { + UnknownNotificationTypeError, +} from "../notification"; +import MastodonNotificationType from "./notification"; namespace MastodonAPI { - /** - * Interface - */ - export interface Interface { - get(path: string, params?: any, headers?: { [key: string]: string }, pathIsFullyQualified?: boolean): Promise> - put(path: string, params?: any, headers?: { [key: string]: string }): Promise> - putForm(path: string, params?: any, headers?: { [key: string]: string }): Promise> - patch(path: string, params?: any, headers?: { [key: string]: string }): Promise> - patchForm(path: string, params?: any, headers?: { [key: string]: string }): Promise> - post(path: string, params?: any, headers?: { [key: string]: string }): Promise> - postForm(path: string, params?: any, headers?: { [key: string]: string }): Promise> - del(path: string, params?: any, headers?: { [key: string]: string }): Promise> - cancel(): void - socket(path: string, stream: string, params?: string): WebSocket - } + /** + * Interface + */ + export interface Interface { + get( + path: string, + params?: any, + headers?: { [key: string]: string }, + pathIsFullyQualified?: boolean, + ): Promise>; + put( + path: string, + params?: any, + headers?: { [key: string]: string }, + ): Promise>; + putForm( + path: string, + params?: any, + headers?: { [key: string]: string }, + ): Promise>; + patch( + path: string, + params?: any, + headers?: { [key: string]: string }, + ): Promise>; + patchForm( + path: string, + params?: any, + headers?: { [key: string]: string }, + ): Promise>; + post( + path: string, + params?: any, + headers?: { [key: string]: string }, + ): Promise>; + postForm( + path: string, + params?: any, + headers?: { [key: string]: string }, + ): Promise>; + del( + path: string, + params?: any, + headers?: { [key: string]: string }, + ): Promise>; + cancel(): void; + socket(path: string, stream: string, params?: string): WebSocket; + } - /** - * Mastodon API client. - * - * Using axios for request, you will handle promises. - */ - export class Client implements Interface { - static DEFAULT_SCOPE = DEFAULT_SCOPE - static DEFAULT_URL = 'https://mastodon.social' - static NO_REDIRECT = NO_REDIRECT + /** + * Mastodon API client. + * + * Using axios for request, you will handle promises. + */ + export class Client implements Interface { + static DEFAULT_SCOPE = DEFAULT_SCOPE; + static DEFAULT_URL = "https://mastodon.social"; + static NO_REDIRECT = NO_REDIRECT; - private accessToken: string | null - private baseUrl: string - private userAgent: string - private abortController: AbortController - private proxyConfig: ProxyConfig | false = false + private accessToken: string | null; + private baseUrl: string; + private userAgent: string; + private abortController: AbortController; + private proxyConfig: ProxyConfig | false = false; - /** - * @param baseUrl hostname or base URL - * @param accessToken access token from OAuth2 authorization - * @param userAgent UserAgent is specified in header on request. - * @param proxyConfig Proxy setting, or set false if don't use proxy. - */ - constructor( - baseUrl: string, - accessToken: string | null = null, - userAgent: string = DEFAULT_UA, - proxyConfig: ProxyConfig | false = false - ) { - this.accessToken = accessToken - this.baseUrl = baseUrl - this.userAgent = userAgent - this.proxyConfig = proxyConfig - this.abortController = new AbortController() - axios.defaults.signal = this.abortController.signal - } + /** + * @param baseUrl hostname or base URL + * @param accessToken access token from OAuth2 authorization + * @param userAgent UserAgent is specified in header on request. + * @param proxyConfig Proxy setting, or set false if don't use proxy. + */ + constructor( + baseUrl: string, + accessToken: string | null = null, + userAgent: string = DEFAULT_UA, + proxyConfig: ProxyConfig | false = false, + ) { + this.accessToken = accessToken; + this.baseUrl = baseUrl; + this.userAgent = userAgent; + this.proxyConfig = proxyConfig; + this.abortController = new AbortController(); + axios.defaults.signal = this.abortController.signal; + } - /** - * GET request to mastodon REST API. - * @param path relative path from baseUrl - * @param params Query parameters - * @param headers Request header object - */ - public async get( - path: string, - params = {}, - headers: { [key: string]: string } = {}, - pathIsFullyQualified = false - ): Promise> { - let options: AxiosRequestConfig = { - params: params, - headers: headers, - maxContentLength: Number.POSITIVE_INFINITY, - maxBodyLength: Number.POSITIVE_INFINITY - } - if (this.accessToken) { - options = objectAssignDeep({}, options, { - headers: { - Authorization: `Bearer ${this.accessToken}` - } - }) - } - if (this.proxyConfig) { - options = Object.assign(options, { - httpAgent: proxyAgent(this.proxyConfig), - httpsAgent: proxyAgent(this.proxyConfig) - }) - } - return axios - .get((pathIsFullyQualified ? '' : this.baseUrl) + path, options) - .catch((err: Error) => { - if (axios.isCancel(err)) { - throw new RequestCanceledError(err.message) - } else { - throw err - } - }) - .then((resp: AxiosResponse) => { - const res: Response = { - data: resp.data, - status: resp.status, - statusText: resp.statusText, - headers: resp.headers - } - return res - }) - } + /** + * GET request to mastodon REST API. + * @param path relative path from baseUrl + * @param params Query parameters + * @param headers Request header object + */ + public async get( + path: string, + params = {}, + headers: { [key: string]: string } = {}, + pathIsFullyQualified = false, + ): Promise> { + let options: AxiosRequestConfig = { + params: params, + headers: headers, + maxContentLength: Number.POSITIVE_INFINITY, + maxBodyLength: Number.POSITIVE_INFINITY, + }; + if (this.accessToken) { + options = objectAssignDeep({}, options, { + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + }); + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpAgent: proxyAgent(this.proxyConfig), + httpsAgent: proxyAgent(this.proxyConfig), + }); + } + return axios + .get((pathIsFullyQualified ? "" : this.baseUrl) + path, options) + .catch((err: Error) => { + if (axios.isCancel(err)) { + throw new RequestCanceledError(err.message); + } else { + throw err; + } + }) + .then((resp: AxiosResponse) => { + const res: Response = { + data: resp.data, + status: resp.status, + statusText: resp.statusText, + headers: resp.headers, + }; + return res; + }); + } - /** - * PUT request to mastodon REST API. - * @param path relative path from baseUrl - * @param params Form data. If you want to post file, please use FormData() - * @param headers Request header object - */ - public async put(path: string, params = {}, headers: { [key: string]: string } = {}): Promise> { - let options: AxiosRequestConfig = { - headers: headers, - maxContentLength: Number.POSITIVE_INFINITY, - maxBodyLength: Number.POSITIVE_INFINITY - } - if (this.accessToken) { - options = objectAssignDeep({}, options, { - headers: { - Authorization: `Bearer ${this.accessToken}` - } - }) - } - if (this.proxyConfig) { - options = Object.assign(options, { - httpAgent: proxyAgent(this.proxyConfig), - httpsAgent: proxyAgent(this.proxyConfig) - }) - } - return axios - .put(this.baseUrl + path, params, options) - .catch((err: Error) => { - if (axios.isCancel(err)) { - throw new RequestCanceledError(err.message) - } else { - throw err - } - }) - .then((resp: AxiosResponse) => { - const res: Response = { - data: resp.data, - status: resp.status, - statusText: resp.statusText, - headers: resp.headers - } - return res - }) - } + /** + * PUT request to mastodon REST API. + * @param path relative path from baseUrl + * @param params Form data. If you want to post file, please use FormData() + * @param headers Request header object + */ + public async put( + path: string, + params = {}, + headers: { [key: string]: string } = {}, + ): Promise> { + let options: AxiosRequestConfig = { + headers: headers, + maxContentLength: Number.POSITIVE_INFINITY, + maxBodyLength: Number.POSITIVE_INFINITY, + }; + if (this.accessToken) { + options = objectAssignDeep({}, options, { + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + }); + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpAgent: proxyAgent(this.proxyConfig), + httpsAgent: proxyAgent(this.proxyConfig), + }); + } + return axios + .put(this.baseUrl + path, params, options) + .catch((err: Error) => { + if (axios.isCancel(err)) { + throw new RequestCanceledError(err.message); + } else { + throw err; + } + }) + .then((resp: AxiosResponse) => { + const res: Response = { + data: resp.data, + status: resp.status, + statusText: resp.statusText, + headers: resp.headers, + }; + return res; + }); + } - /** - * PUT request to mastodon REST API for multipart. - * @param path relative path from baseUrl - * @param params Form data. If you want to post file, please use FormData() - * @param headers Request header object - */ - public async putForm(path: string, params = {}, headers: { [key: string]: string } = {}): Promise> { - let options: AxiosRequestConfig = { - headers: headers, - maxContentLength: Number.POSITIVE_INFINITY, - maxBodyLength: Number.POSITIVE_INFINITY - } - if (this.accessToken) { - options = objectAssignDeep({}, options, { - headers: { - Authorization: `Bearer ${this.accessToken}` - } - }) - } - if (this.proxyConfig) { - options = Object.assign(options, { - httpAgent: proxyAgent(this.proxyConfig), - httpsAgent: proxyAgent(this.proxyConfig) - }) - } - return axios - .putForm(this.baseUrl + path, params, options) - .catch((err: Error) => { - if (axios.isCancel(err)) { - throw new RequestCanceledError(err.message) - } else { - throw err - } - }) - .then((resp: AxiosResponse) => { - const res: Response = { - data: resp.data, - status: resp.status, - statusText: resp.statusText, - headers: resp.headers - } - return res - }) - } + /** + * PUT request to mastodon REST API for multipart. + * @param path relative path from baseUrl + * @param params Form data. If you want to post file, please use FormData() + * @param headers Request header object + */ + public async putForm( + path: string, + params = {}, + headers: { [key: string]: string } = {}, + ): Promise> { + let options: AxiosRequestConfig = { + headers: headers, + maxContentLength: Number.POSITIVE_INFINITY, + maxBodyLength: Number.POSITIVE_INFINITY, + }; + if (this.accessToken) { + options = objectAssignDeep({}, options, { + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + }); + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpAgent: proxyAgent(this.proxyConfig), + httpsAgent: proxyAgent(this.proxyConfig), + }); + } + return axios + .putForm(this.baseUrl + path, params, options) + .catch((err: Error) => { + if (axios.isCancel(err)) { + throw new RequestCanceledError(err.message); + } else { + throw err; + } + }) + .then((resp: AxiosResponse) => { + const res: Response = { + data: resp.data, + status: resp.status, + statusText: resp.statusText, + headers: resp.headers, + }; + return res; + }); + } - /** - * PATCH request to mastodon REST API. - * @param path relative path from baseUrl - * @param params Form data. If you want to post file, please use FormData() - * @param headers Request header object - */ - public async patch(path: string, params = {}, headers: { [key: string]: string } = {}): Promise> { - let options: AxiosRequestConfig = { - headers: headers, - maxContentLength: Number.POSITIVE_INFINITY, - maxBodyLength: Number.POSITIVE_INFINITY - } - if (this.accessToken) { - options = objectAssignDeep({}, options, { - headers: { - Authorization: `Bearer ${this.accessToken}` - } - }) - } - if (this.proxyConfig) { - options = Object.assign(options, { - httpAgent: proxyAgent(this.proxyConfig), - httpsAgent: proxyAgent(this.proxyConfig) - }) - } - return axios - .patch(this.baseUrl + path, params, options) - .catch((err: Error) => { - if (axios.isCancel(err)) { - throw new RequestCanceledError(err.message) - } else { - throw err - } - }) - .then((resp: AxiosResponse) => { - const res: Response = { - data: resp.data, - status: resp.status, - statusText: resp.statusText, - headers: resp.headers - } - return res - }) - } + /** + * PATCH request to mastodon REST API. + * @param path relative path from baseUrl + * @param params Form data. If you want to post file, please use FormData() + * @param headers Request header object + */ + public async patch( + path: string, + params = {}, + headers: { [key: string]: string } = {}, + ): Promise> { + let options: AxiosRequestConfig = { + headers: headers, + maxContentLength: Number.POSITIVE_INFINITY, + maxBodyLength: Number.POSITIVE_INFINITY, + }; + if (this.accessToken) { + options = objectAssignDeep({}, options, { + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + }); + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpAgent: proxyAgent(this.proxyConfig), + httpsAgent: proxyAgent(this.proxyConfig), + }); + } + return axios + .patch(this.baseUrl + path, params, options) + .catch((err: Error) => { + if (axios.isCancel(err)) { + throw new RequestCanceledError(err.message); + } else { + throw err; + } + }) + .then((resp: AxiosResponse) => { + const res: Response = { + data: resp.data, + status: resp.status, + statusText: resp.statusText, + headers: resp.headers, + }; + return res; + }); + } - /** - * PATCH request to mastodon REST API for multipart. - * @param path relative path from baseUrl - * @param params Form data. If you want to post file, please use FormData() - * @param headers Request header object - */ - public async patchForm(path: string, params = {}, headers: { [key: string]: string } = {}): Promise> { - let options: AxiosRequestConfig = { - headers: headers, - maxContentLength: Number.POSITIVE_INFINITY, - maxBodyLength: Number.POSITIVE_INFINITY - } - if (this.accessToken) { - options = objectAssignDeep({}, options, { - headers: { - Authorization: `Bearer ${this.accessToken}` - } - }) - } - if (this.proxyConfig) { - options = Object.assign(options, { - httpAgent: proxyAgent(this.proxyConfig), - httpsAgent: proxyAgent(this.proxyConfig) - }) - } - return axios - .patchForm(this.baseUrl + path, params, options) - .catch((err: Error) => { - if (axios.isCancel(err)) { - throw new RequestCanceledError(err.message) - } else { - throw err - } - }) - .then((resp: AxiosResponse) => { - const res: Response = { - data: resp.data, - status: resp.status, - statusText: resp.statusText, - headers: resp.headers - } - return res - }) - } + /** + * PATCH request to mastodon REST API for multipart. + * @param path relative path from baseUrl + * @param params Form data. If you want to post file, please use FormData() + * @param headers Request header object + */ + public async patchForm( + path: string, + params = {}, + headers: { [key: string]: string } = {}, + ): Promise> { + let options: AxiosRequestConfig = { + headers: headers, + maxContentLength: Number.POSITIVE_INFINITY, + maxBodyLength: Number.POSITIVE_INFINITY, + }; + if (this.accessToken) { + options = objectAssignDeep({}, options, { + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + }); + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpAgent: proxyAgent(this.proxyConfig), + httpsAgent: proxyAgent(this.proxyConfig), + }); + } + return axios + .patchForm(this.baseUrl + path, params, options) + .catch((err: Error) => { + if (axios.isCancel(err)) { + throw new RequestCanceledError(err.message); + } else { + throw err; + } + }) + .then((resp: AxiosResponse) => { + const res: Response = { + data: resp.data, + status: resp.status, + statusText: resp.statusText, + headers: resp.headers, + }; + return res; + }); + } - /** - * POST request to mastodon REST API. - * @param path relative path from baseUrl - * @param params Form data - * @param headers Request header object - */ - public async post(path: string, params = {}, headers: { [key: string]: string } = {}): Promise> { - let options: AxiosRequestConfig = { - headers: headers, - maxContentLength: Number.POSITIVE_INFINITY, - maxBodyLength: Number.POSITIVE_INFINITY - } - if (this.accessToken) { - options = objectAssignDeep({}, options, { - headers: { - Authorization: `Bearer ${this.accessToken}` - } - }) - } - if (this.proxyConfig) { - options = Object.assign(options, { - httpAgent: proxyAgent(this.proxyConfig), - httpsAgent: proxyAgent(this.proxyConfig) - }) - } - return axios.post(this.baseUrl + path, params, options).then((resp: AxiosResponse) => { - const res: Response = { - data: resp.data, - status: resp.status, - statusText: resp.statusText, - headers: resp.headers - } - return res - }) - } + /** + * POST request to mastodon REST API. + * @param path relative path from baseUrl + * @param params Form data + * @param headers Request header object + */ + public async post( + path: string, + params = {}, + headers: { [key: string]: string } = {}, + ): Promise> { + let options: AxiosRequestConfig = { + headers: headers, + maxContentLength: Number.POSITIVE_INFINITY, + maxBodyLength: Number.POSITIVE_INFINITY, + }; + if (this.accessToken) { + options = objectAssignDeep({}, options, { + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + }); + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpAgent: proxyAgent(this.proxyConfig), + httpsAgent: proxyAgent(this.proxyConfig), + }); + } + return axios + .post(this.baseUrl + path, params, options) + .then((resp: AxiosResponse) => { + const res: Response = { + data: resp.data, + status: resp.status, + statusText: resp.statusText, + headers: resp.headers, + }; + return res; + }); + } - /** - * POST request to mastodon REST API for multipart. - * @param path relative path from baseUrl - * @param params Form data - * @param headers Request header object - */ - public async postForm(path: string, params = {}, headers: { [key: string]: string } = {}): Promise> { - let options: AxiosRequestConfig = { - headers: headers, - maxContentLength: Number.POSITIVE_INFINITY, - maxBodyLength: Number.POSITIVE_INFINITY - } - if (this.accessToken) { - options = objectAssignDeep({}, options, { - headers: { - Authorization: `Bearer ${this.accessToken}` - } - }) - } - if (this.proxyConfig) { - options = Object.assign(options, { - httpAgent: proxyAgent(this.proxyConfig), - httpsAgent: proxyAgent(this.proxyConfig) - }) - } - return axios.postForm(this.baseUrl + path, params, options).then((resp: AxiosResponse) => { - const res: Response = { - data: resp.data, - status: resp.status, - statusText: resp.statusText, - headers: resp.headers - } - return res - }) - } + /** + * POST request to mastodon REST API for multipart. + * @param path relative path from baseUrl + * @param params Form data + * @param headers Request header object + */ + public async postForm( + path: string, + params = {}, + headers: { [key: string]: string } = {}, + ): Promise> { + let options: AxiosRequestConfig = { + headers: headers, + maxContentLength: Number.POSITIVE_INFINITY, + maxBodyLength: Number.POSITIVE_INFINITY, + }; + if (this.accessToken) { + options = objectAssignDeep({}, options, { + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + }); + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpAgent: proxyAgent(this.proxyConfig), + httpsAgent: proxyAgent(this.proxyConfig), + }); + } + return axios + .postForm(this.baseUrl + path, params, options) + .then((resp: AxiosResponse) => { + const res: Response = { + data: resp.data, + status: resp.status, + statusText: resp.statusText, + headers: resp.headers, + }; + return res; + }); + } - /** - * DELETE request to mastodon REST API. - * @param path relative path from baseUrl - * @param params Form data - * @param headers Request header object - */ - public async del(path: string, params = {}, headers: { [key: string]: string } = {}): Promise> { - let options: AxiosRequestConfig = { - data: params, - headers: headers, - maxContentLength: Number.POSITIVE_INFINITY, - maxBodyLength: Number.POSITIVE_INFINITY - } - if (this.accessToken) { - options = objectAssignDeep({}, options, { - headers: { - Authorization: `Bearer ${this.accessToken}` - } - }) - } - if (this.proxyConfig) { - options = Object.assign(options, { - httpAgent: proxyAgent(this.proxyConfig), - httpsAgent: proxyAgent(this.proxyConfig) - }) - } - return axios - .delete(this.baseUrl + path, options) - .catch((err: Error) => { - if (axios.isCancel(err)) { - throw new RequestCanceledError(err.message) - } else { - throw err - } - }) - .then((resp: AxiosResponse) => { - const res: Response = { - data: resp.data, - status: resp.status, - statusText: resp.statusText, - headers: resp.headers - } - return res - }) - } + /** + * DELETE request to mastodon REST API. + * @param path relative path from baseUrl + * @param params Form data + * @param headers Request header object + */ + public async del( + path: string, + params = {}, + headers: { [key: string]: string } = {}, + ): Promise> { + let options: AxiosRequestConfig = { + data: params, + headers: headers, + maxContentLength: Number.POSITIVE_INFINITY, + maxBodyLength: Number.POSITIVE_INFINITY, + }; + if (this.accessToken) { + options = objectAssignDeep({}, options, { + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + }); + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpAgent: proxyAgent(this.proxyConfig), + httpsAgent: proxyAgent(this.proxyConfig), + }); + } + return axios + .delete(this.baseUrl + path, options) + .catch((err: Error) => { + if (axios.isCancel(err)) { + throw new RequestCanceledError(err.message); + } else { + throw err; + } + }) + .then((resp: AxiosResponse) => { + const res: Response = { + data: resp.data, + status: resp.status, + statusText: resp.statusText, + headers: resp.headers, + }; + return res; + }); + } - /** - * Cancel all requests in this instance. - * @returns void - */ - public cancel(): void { - return this.abortController.abort() - } + /** + * Cancel all requests in this instance. + * @returns void + */ + public cancel(): void { + return this.abortController.abort(); + } - /** - * Get connection and receive websocket connection for Pleroma API. - * - * @param path relative path from baseUrl: normally it is `/streaming`. - * @param stream Stream name, please refer: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/mastodon_api/mastodon_socket.ex#L19-28 - * @returns WebSocket, which inherits from EventEmitter - */ - public socket(path: string, stream: string, params?: string): WebSocket { - if (!this.accessToken) { - throw new Error('accessToken is required') - } - const url = this.baseUrl + path - const streaming = new WebSocket(url, stream, params, this.accessToken, this.userAgent, this.proxyConfig) - process.nextTick(() => { - streaming.start() - }) - return streaming - } - } + /** + * Get connection and receive websocket connection for Pleroma API. + * + * @param path relative path from baseUrl: normally it is `/streaming`. + * @param stream Stream name, please refer: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/mastodon_api/mastodon_socket.ex#L19-28 + * @returns WebSocket, which inherits from EventEmitter + */ + public socket(path: string, stream: string, params?: string): WebSocket { + if (!this.accessToken) { + throw new Error("accessToken is required"); + } + const url = this.baseUrl + path; + const streaming = new WebSocket( + url, + stream, + params, + this.accessToken, + this.userAgent, + this.proxyConfig, + ); + process.nextTick(() => { + streaming.start(); + }); + return streaming; + } + } - export namespace Entity { - export type Account = MastodonEntity.Account - export type Activity = MastodonEntity.Activity - export type Announcement = MastodonEntity.Announcement - export type Application = MastodonEntity.Application - export type AsyncAttachment = MegalodonEntity.AsyncAttachment - export type Attachment = MastodonEntity.Attachment - export type Card = MastodonEntity.Card - export type Context = MastodonEntity.Context - export type Conversation = MastodonEntity.Conversation - export type Emoji = MastodonEntity.Emoji - export type FeaturedTag = MastodonEntity.FeaturedTag - export type Field = MastodonEntity.Field - export type Filter = MastodonEntity.Filter - export type History = MastodonEntity.History - export type IdentityProof = MastodonEntity.IdentityProof - export type Instance = MastodonEntity.Instance - export type List = MastodonEntity.List - export type Marker = MastodonEntity.Marker - export type Mention = MastodonEntity.Mention - export type Notification = MastodonEntity.Notification - export type Poll = MastodonEntity.Poll - export type PollOption = MastodonEntity.PollOption - export type Preferences = MastodonEntity.Preferences - export type PushSubscription = MastodonEntity.PushSubscription - export type Relationship = MastodonEntity.Relationship - export type Report = MastodonEntity.Report - export type Results = MastodonEntity.Results - export type Role = MastodonEntity.Role - export type ScheduledStatus = MastodonEntity.ScheduledStatus - export type Source = MastodonEntity.Source - export type Stats = MastodonEntity.Stats - export type Status = MastodonEntity.Status - export type StatusParams = MastodonEntity.StatusParams - export type StatusSource = MastodonEntity.StatusSource - export type Tag = MastodonEntity.Tag - export type Token = MastodonEntity.Token - export type URLs = MastodonEntity.URLs - } + export namespace Entity { + export type Account = MastodonEntity.Account; + export type Activity = MastodonEntity.Activity; + export type Announcement = MastodonEntity.Announcement; + export type Application = MastodonEntity.Application; + export type AsyncAttachment = MegalodonEntity.AsyncAttachment; + export type Attachment = MastodonEntity.Attachment; + export type Card = MastodonEntity.Card; + export type Context = MastodonEntity.Context; + export type Conversation = MastodonEntity.Conversation; + export type Emoji = MastodonEntity.Emoji; + export type FeaturedTag = MastodonEntity.FeaturedTag; + export type Field = MastodonEntity.Field; + export type Filter = MastodonEntity.Filter; + export type History = MastodonEntity.History; + export type IdentityProof = MastodonEntity.IdentityProof; + export type Instance = MastodonEntity.Instance; + export type List = MastodonEntity.List; + export type Marker = MastodonEntity.Marker; + export type Mention = MastodonEntity.Mention; + export type Notification = MastodonEntity.Notification; + export type Poll = MastodonEntity.Poll; + export type PollOption = MastodonEntity.PollOption; + export type Preferences = MastodonEntity.Preferences; + export type PushSubscription = MastodonEntity.PushSubscription; + export type Relationship = MastodonEntity.Relationship; + export type Report = MastodonEntity.Report; + export type Results = MastodonEntity.Results; + export type Role = MastodonEntity.Role; + export type ScheduledStatus = MastodonEntity.ScheduledStatus; + export type Source = MastodonEntity.Source; + export type Stats = MastodonEntity.Stats; + export type Status = MastodonEntity.Status; + export type StatusParams = MastodonEntity.StatusParams; + export type StatusSource = MastodonEntity.StatusSource; + export type Tag = MastodonEntity.Tag; + export type Token = MastodonEntity.Token; + export type URLs = MastodonEntity.URLs; + } - export namespace Converter { - export const encodeNotificationType = ( - t: MegalodonEntity.NotificationType - ): MastodonEntity.NotificationType | UnknownNotificationTypeError => { - switch (t) { - case NotificationType.Follow: - return MastodonNotificationType.Follow - case NotificationType.Favourite: - return MastodonNotificationType.Favourite - case NotificationType.Reblog: - return MastodonNotificationType.Reblog - case NotificationType.Mention: - return MastodonNotificationType.Mention - case NotificationType.FollowRequest: - return MastodonNotificationType.FollowRequest - case NotificationType.Status: - return MastodonNotificationType.Status - case NotificationType.PollExpired: - return MastodonNotificationType.Poll - case NotificationType.Update: - return MastodonNotificationType.Update - case NotificationType.AdminSignup: - return MastodonNotificationType.AdminSignup - case NotificationType.AdminReport: - return MastodonNotificationType.AdminReport - default: - return new UnknownNotificationTypeError() - } - } + export namespace Converter { + export const encodeNotificationType = ( + t: MegalodonEntity.NotificationType, + ): MastodonEntity.NotificationType | UnknownNotificationTypeError => { + switch (t) { + case NotificationType.Follow: + return MastodonNotificationType.Follow; + case NotificationType.Favourite: + return MastodonNotificationType.Favourite; + case NotificationType.Reblog: + return MastodonNotificationType.Reblog; + case NotificationType.Mention: + return MastodonNotificationType.Mention; + case NotificationType.FollowRequest: + return MastodonNotificationType.FollowRequest; + case NotificationType.Status: + return MastodonNotificationType.Status; + case NotificationType.PollExpired: + return MastodonNotificationType.Poll; + case NotificationType.Update: + return MastodonNotificationType.Update; + case NotificationType.AdminSignup: + return MastodonNotificationType.AdminSignup; + case NotificationType.AdminReport: + return MastodonNotificationType.AdminReport; + default: + return new UnknownNotificationTypeError(); + } + }; - export const decodeNotificationType = ( - t: MastodonEntity.NotificationType - ): MegalodonEntity.NotificationType | UnknownNotificationTypeError => { - switch (t) { - case MastodonNotificationType.Follow: - return NotificationType.Follow - case MastodonNotificationType.Favourite: - return NotificationType.Favourite - case MastodonNotificationType.Mention: - return NotificationType.Mention - case MastodonNotificationType.Reblog: - return NotificationType.Reblog - case MastodonNotificationType.FollowRequest: - return NotificationType.FollowRequest - case MastodonNotificationType.Status: - return NotificationType.Status - case MastodonNotificationType.Poll: - return NotificationType.PollExpired - case MastodonNotificationType.Update: - return NotificationType.Update - case MastodonNotificationType.AdminSignup: - return NotificationType.AdminSignup - case MastodonNotificationType.AdminReport: - return NotificationType.AdminReport - default: - return new UnknownNotificationTypeError() - } - } + export const decodeNotificationType = ( + t: MastodonEntity.NotificationType, + ): MegalodonEntity.NotificationType | UnknownNotificationTypeError => { + switch (t) { + case MastodonNotificationType.Follow: + return NotificationType.Follow; + case MastodonNotificationType.Favourite: + return NotificationType.Favourite; + case MastodonNotificationType.Mention: + return NotificationType.Mention; + case MastodonNotificationType.Reblog: + return NotificationType.Reblog; + case MastodonNotificationType.FollowRequest: + return NotificationType.FollowRequest; + case MastodonNotificationType.Status: + return NotificationType.Status; + case MastodonNotificationType.Poll: + return NotificationType.PollExpired; + case MastodonNotificationType.Update: + return NotificationType.Update; + case MastodonNotificationType.AdminSignup: + return NotificationType.AdminSignup; + case MastodonNotificationType.AdminReport: + return NotificationType.AdminReport; + default: + return new UnknownNotificationTypeError(); + } + }; - export const account = (a: Entity.Account): MegalodonEntity.Account => a - export const activity = (a: Entity.Activity): MegalodonEntity.Activity => a - export const announcement = (a: Entity.Announcement): MegalodonEntity.Announcement => a - export const application = (a: Entity.Application): MegalodonEntity.Application => a - export const attachment = (a: Entity.Attachment): MegalodonEntity.Attachment => a - export const async_attachment = (a: Entity.AsyncAttachment) => { - if (a.url) { - return { - id: a.id, - type: a.type, - url: a.url!, - remote_url: a.remote_url, - preview_url: a.preview_url, - text_url: a.text_url, - meta: a.meta, - description: a.description, - blurhash: a.blurhash - } as MegalodonEntity.Attachment - } else { - return a as MegalodonEntity.AsyncAttachment - } - } - export const card = (c: Entity.Card): MegalodonEntity.Card => c - export const context = (c: Entity.Context): MegalodonEntity.Context => ({ - ancestors: Array.isArray(c.ancestors) ? c.ancestors.map(a => status(a)) : [], - descendants: Array.isArray(c.descendants) ? c.descendants.map(d => status(d)) : [] - }) - export const conversation = (c: Entity.Conversation): MegalodonEntity.Conversation => ({ - id: c.id, - accounts: Array.isArray(c.accounts) ? c.accounts.map(a => account(a)) : [], - last_status: c.last_status ? status(c.last_status) : null, - unread: c.unread - }) - export const emoji = (e: Entity.Emoji): MegalodonEntity.Emoji => e - export const featured_tag = (e: Entity.FeaturedTag): MegalodonEntity.FeaturedTag => e - export const field = (f: Entity.Field): MegalodonEntity.Field => f - export const filter = (f: Entity.Filter): MegalodonEntity.Filter => f - export const history = (h: Entity.History): MegalodonEntity.History => h - export const identity_proof = (i: Entity.IdentityProof): MegalodonEntity.IdentityProof => i - export const instance = (i: Entity.Instance): MegalodonEntity.Instance => i - export const list = (l: Entity.List): MegalodonEntity.List => l - export const marker = (m: Entity.Marker | Record): MegalodonEntity.Marker | Record => m - export const mention = (m: Entity.Mention): MegalodonEntity.Mention => m - export const notification = (n: Entity.Notification): MegalodonEntity.Notification | UnknownNotificationTypeError => { - const notificationType = decodeNotificationType(n.type) - if (notificationType instanceof UnknownNotificationTypeError) return notificationType - if (n.status) { - return { - account: account(n.account), - created_at: n.created_at, - id: n.id, - status: status(n.status), - type: notificationType - } - } else { - return { - account: account(n.account), - created_at: n.created_at, - id: n.id, - type: notificationType - } - } - } - export const poll = (p: Entity.Poll): MegalodonEntity.Poll => p - export const poll_option = (p: Entity.PollOption): MegalodonEntity.PollOption => p - export const preferences = (p: Entity.Preferences): MegalodonEntity.Preferences => p - export const push_subscription = (p: Entity.PushSubscription): MegalodonEntity.PushSubscription => p - export const relationship = (r: Entity.Relationship): MegalodonEntity.Relationship => r - export const report = (r: Entity.Report): MegalodonEntity.Report => r - export const results = (r: Entity.Results): MegalodonEntity.Results => ({ - accounts: Array.isArray(r.accounts) ? r.accounts.map(a => account(a)) : [], - statuses: Array.isArray(r.statuses) ? r.statuses.map(s => status(s)) : [], - hashtags: Array.isArray(r.hashtags) ? r.hashtags.map(h => tag(h)) : [] - }) - export const scheduled_status = (s: Entity.ScheduledStatus): MegalodonEntity.ScheduledStatus => s - export const source = (s: Entity.Source): MegalodonEntity.Source => s - export const stats = (s: Entity.Stats): MegalodonEntity.Stats => s - export const status = (s: Entity.Status): MegalodonEntity.Status => ({ - id: s.id, - uri: s.uri, - url: s.url, - account: account(s.account), - in_reply_to_id: s.in_reply_to_id, - in_reply_to_account_id: s.in_reply_to_account_id, - reblog: s.reblog ? status(s.reblog) : s.quote ? status(s.quote) : null, - content: s.content, - plain_content: null, - created_at: s.created_at, - emojis: Array.isArray(s.emojis) ? s.emojis.map(e => emoji(e)) : [], - replies_count: s.replies_count, - reblogs_count: s.reblogs_count, - favourites_count: s.favourites_count, - reblogged: s.reblogged, - favourited: s.favourited, - muted: s.muted, - sensitive: s.sensitive, - spoiler_text: s.spoiler_text, - visibility: s.visibility, - media_attachments: Array.isArray(s.media_attachments) ? s.media_attachments.map(m => attachment(m)) : [], - mentions: Array.isArray(s.mentions) ? s.mentions.map(m => mention(m)) : [], - tags: s.tags, - card: s.card ? card(s.card) : null, - poll: s.poll ? poll(s.poll) : null, - application: s.application ? application(s.application) : null, - language: s.language, - pinned: s.pinned, - emoji_reactions: [], - bookmarked: s.bookmarked ? s.bookmarked : false, - // Now quote is supported only fedibird.com. - quote: s.quote !== undefined && s.quote !== null - }) - export const status_params = (s: Entity.StatusParams): MegalodonEntity.StatusParams => s - export const status_source = (s: Entity.StatusSource): MegalodonEntity.StatusSource => s - export const tag = (t: Entity.Tag): MegalodonEntity.Tag => t - export const token = (t: Entity.Token): MegalodonEntity.Token => t - export const urls = (u: Entity.URLs): MegalodonEntity.URLs => u - } + export const account = (a: Entity.Account): MegalodonEntity.Account => a; + export const activity = (a: Entity.Activity): MegalodonEntity.Activity => a; + export const announcement = ( + a: Entity.Announcement, + ): MegalodonEntity.Announcement => a; + export const application = ( + a: Entity.Application, + ): MegalodonEntity.Application => a; + export const attachment = ( + a: Entity.Attachment, + ): MegalodonEntity.Attachment => a; + export const async_attachment = (a: Entity.AsyncAttachment) => { + if (a.url) { + return { + id: a.id, + type: a.type, + url: a.url!, + remote_url: a.remote_url, + preview_url: a.preview_url, + text_url: a.text_url, + meta: a.meta, + description: a.description, + blurhash: a.blurhash, + } as MegalodonEntity.Attachment; + } else { + return a as MegalodonEntity.AsyncAttachment; + } + }; + export const card = (c: Entity.Card): MegalodonEntity.Card => c; + export const context = (c: Entity.Context): MegalodonEntity.Context => ({ + ancestors: Array.isArray(c.ancestors) + ? c.ancestors.map((a) => status(a)) + : [], + descendants: Array.isArray(c.descendants) + ? c.descendants.map((d) => status(d)) + : [], + }); + export const conversation = ( + c: Entity.Conversation, + ): MegalodonEntity.Conversation => ({ + id: c.id, + accounts: Array.isArray(c.accounts) + ? c.accounts.map((a) => account(a)) + : [], + last_status: c.last_status ? status(c.last_status) : null, + unread: c.unread, + }); + export const emoji = (e: Entity.Emoji): MegalodonEntity.Emoji => e; + export const featured_tag = ( + e: Entity.FeaturedTag, + ): MegalodonEntity.FeaturedTag => e; + export const field = (f: Entity.Field): MegalodonEntity.Field => f; + export const filter = (f: Entity.Filter): MegalodonEntity.Filter => f; + export const history = (h: Entity.History): MegalodonEntity.History => h; + export const identity_proof = ( + i: Entity.IdentityProof, + ): MegalodonEntity.IdentityProof => i; + export const instance = (i: Entity.Instance): MegalodonEntity.Instance => i; + export const list = (l: Entity.List): MegalodonEntity.List => l; + export const marker = ( + m: Entity.Marker | Record, + ): MegalodonEntity.Marker | Record => m; + export const mention = (m: Entity.Mention): MegalodonEntity.Mention => m; + export const notification = ( + n: Entity.Notification, + ): MegalodonEntity.Notification | UnknownNotificationTypeError => { + const notificationType = decodeNotificationType(n.type); + if (notificationType instanceof UnknownNotificationTypeError) + return notificationType; + if (n.status) { + return { + account: account(n.account), + created_at: n.created_at, + id: n.id, + status: status(n.status), + type: notificationType, + }; + } else { + return { + account: account(n.account), + created_at: n.created_at, + id: n.id, + type: notificationType, + }; + } + }; + export const poll = (p: Entity.Poll): MegalodonEntity.Poll => p; + export const poll_option = ( + p: Entity.PollOption, + ): MegalodonEntity.PollOption => p; + export const preferences = ( + p: Entity.Preferences, + ): MegalodonEntity.Preferences => p; + export const push_subscription = ( + p: Entity.PushSubscription, + ): MegalodonEntity.PushSubscription => p; + export const relationship = ( + r: Entity.Relationship, + ): MegalodonEntity.Relationship => r; + export const report = (r: Entity.Report): MegalodonEntity.Report => r; + export const results = (r: Entity.Results): MegalodonEntity.Results => ({ + accounts: Array.isArray(r.accounts) + ? r.accounts.map((a) => account(a)) + : [], + statuses: Array.isArray(r.statuses) + ? r.statuses.map((s) => status(s)) + : [], + hashtags: Array.isArray(r.hashtags) ? r.hashtags.map((h) => tag(h)) : [], + }); + export const scheduled_status = ( + s: Entity.ScheduledStatus, + ): MegalodonEntity.ScheduledStatus => s; + export const source = (s: Entity.Source): MegalodonEntity.Source => s; + export const stats = (s: Entity.Stats): MegalodonEntity.Stats => s; + export const status = (s: Entity.Status): MegalodonEntity.Status => ({ + id: s.id, + uri: s.uri, + url: s.url, + account: account(s.account), + in_reply_to_id: s.in_reply_to_id, + in_reply_to_account_id: s.in_reply_to_account_id, + reblog: s.reblog ? status(s.reblog) : s.quote ? status(s.quote) : null, + content: s.content, + plain_content: null, + created_at: s.created_at, + emojis: Array.isArray(s.emojis) ? s.emojis.map((e) => emoji(e)) : [], + replies_count: s.replies_count, + reblogs_count: s.reblogs_count, + favourites_count: s.favourites_count, + reblogged: s.reblogged, + favourited: s.favourited, + muted: s.muted, + sensitive: s.sensitive, + spoiler_text: s.spoiler_text, + visibility: s.visibility, + media_attachments: Array.isArray(s.media_attachments) + ? s.media_attachments.map((m) => attachment(m)) + : [], + mentions: Array.isArray(s.mentions) + ? s.mentions.map((m) => mention(m)) + : [], + tags: s.tags, + card: s.card ? card(s.card) : null, + poll: s.poll ? poll(s.poll) : null, + application: s.application ? application(s.application) : null, + language: s.language, + pinned: s.pinned, + emoji_reactions: [], + bookmarked: s.bookmarked ? s.bookmarked : false, + // Now quote is supported only fedibird.com. + quote: s.quote !== undefined && s.quote !== null, + }); + export const status_params = ( + s: Entity.StatusParams, + ): MegalodonEntity.StatusParams => s; + export const status_source = ( + s: Entity.StatusSource, + ): MegalodonEntity.StatusSource => s; + export const tag = (t: Entity.Tag): MegalodonEntity.Tag => t; + export const token = (t: Entity.Token): MegalodonEntity.Token => t; + export const urls = (u: Entity.URLs): MegalodonEntity.URLs => u; + } } -export default MastodonAPI +export default MastodonAPI; diff --git a/packages/megalodon/src/mastodon/notification.ts b/packages/megalodon/src/mastodon/notification.ts index ca9fb2d56a..83e9952053 100644 --- a/packages/megalodon/src/mastodon/notification.ts +++ b/packages/megalodon/src/mastodon/notification.ts @@ -1,16 +1,17 @@ -import type MastodonEntity from './entity' +import type MastodonEntity from "./entity"; namespace MastodonNotificationType { - export const Mention: MastodonEntity.NotificationType = 'mention' - export const Reblog: MastodonEntity.NotificationType = 'reblog' - export const Favourite: MastodonEntity.NotificationType = 'favourite' - export const Follow: MastodonEntity.NotificationType = 'follow' - export const Poll: MastodonEntity.NotificationType = 'poll' - export const FollowRequest: MastodonEntity.NotificationType = 'follow_request' - export const Status: MastodonEntity.NotificationType = 'status' - export const Update: MastodonEntity.NotificationType = 'update' - export const AdminSignup: MastodonEntity.NotificationType = 'admin.sign_up' - export const AdminReport: MastodonEntity.NotificationType = 'admin.report' + export const Mention: MastodonEntity.NotificationType = "mention"; + export const Reblog: MastodonEntity.NotificationType = "reblog"; + export const Favourite: MastodonEntity.NotificationType = "favourite"; + export const Follow: MastodonEntity.NotificationType = "follow"; + export const Poll: MastodonEntity.NotificationType = "poll"; + export const FollowRequest: MastodonEntity.NotificationType = + "follow_request"; + export const Status: MastodonEntity.NotificationType = "status"; + export const Update: MastodonEntity.NotificationType = "update"; + export const AdminSignup: MastodonEntity.NotificationType = "admin.sign_up"; + export const AdminReport: MastodonEntity.NotificationType = "admin.report"; } -export default MastodonNotificationType +export default MastodonNotificationType; diff --git a/packages/megalodon/src/mastodon/web_socket.ts b/packages/megalodon/src/mastodon/web_socket.ts index 0b6d2f6fc5..2c6aa9ec98 100644 --- a/packages/megalodon/src/mastodon/web_socket.ts +++ b/packages/megalodon/src/mastodon/web_socket.ts @@ -1,292 +1,326 @@ -import WS from 'ws' -import dayjs, { type Dayjs } from 'dayjs' -import { EventEmitter } from 'events' -import proxyAgent, { type ProxyConfig } from '../proxy_config' -import type { WebSocketInterface } from '../megalodon' -import MastodonAPI from './api_client' -import { UnknownNotificationTypeError } from '../notification' +import WS from "ws"; +import dayjs, { type Dayjs } from "dayjs"; +import { EventEmitter } from "events"; +import proxyAgent, { type ProxyConfig } from "../proxy_config"; +import type { WebSocketInterface } from "../megalodon"; +import MastodonAPI from "./api_client"; +import { UnknownNotificationTypeError } from "../notification"; /** * WebSocket * Pleroma is not support streaming. It is support websocket instead of streaming. * So this class connect to Phoenix websocket for Pleroma. */ -export default class WebSocket extends EventEmitter implements WebSocketInterface { - public url: string - public stream: string - public params: string | null - public parser: Parser - public headers: { [key: string]: string } - public proxyConfig: ProxyConfig | false = false - private _accessToken: string - private _reconnectInterval: number - private _reconnectMaxAttempts: number - private _reconnectCurrentAttempts: number - private _connectionClosed: boolean - private _client: WS | null - private _pongReceivedTimestamp: Dayjs - private _heartbeatInterval = 60000 - private _pongWaiting = false +export default class WebSocket + extends EventEmitter + implements WebSocketInterface +{ + public url: string; + public stream: string; + public params: string | null; + public parser: Parser; + public headers: { [key: string]: string }; + public proxyConfig: ProxyConfig | false = false; + private _accessToken: string; + private _reconnectInterval: number; + private _reconnectMaxAttempts: number; + private _reconnectCurrentAttempts: number; + private _connectionClosed: boolean; + private _client: WS | null; + private _pongReceivedTimestamp: Dayjs; + private _heartbeatInterval = 60000; + private _pongWaiting = false; - /** - * @param url Full url of websocket: e.g. https://pleroma.io/api/v1/streaming - * @param stream Stream name, please refer: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/mastodon_api/mastodon_socket.ex#L19-28 - * @param accessToken The access token. - * @param userAgent The specified User Agent. - * @param proxyConfig Proxy setting, or set false if don't use proxy. - */ - constructor( - url: string, - stream: string, - params: string | undefined, - accessToken: string, - userAgent: string, - proxyConfig: ProxyConfig | false = false - ) { - super() - this.url = url - this.stream = stream - if (params === undefined) { - this.params = null - } else { - this.params = params - } - this.parser = new Parser() - this.headers = { - 'User-Agent': userAgent - } - this.proxyConfig = proxyConfig - this._accessToken = accessToken - this._reconnectInterval = 10000 - this._reconnectMaxAttempts = Number.POSITIVE_INFINITY - this._reconnectCurrentAttempts = 0 - this._connectionClosed = false - this._client = null - this._pongReceivedTimestamp = dayjs() - } + /** + * @param url Full url of websocket: e.g. https://pleroma.io/api/v1/streaming + * @param stream Stream name, please refer: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/mastodon_api/mastodon_socket.ex#L19-28 + * @param accessToken The access token. + * @param userAgent The specified User Agent. + * @param proxyConfig Proxy setting, or set false if don't use proxy. + */ + constructor( + url: string, + stream: string, + params: string | undefined, + accessToken: string, + userAgent: string, + proxyConfig: ProxyConfig | false = false, + ) { + super(); + this.url = url; + this.stream = stream; + if (params === undefined) { + this.params = null; + } else { + this.params = params; + } + this.parser = new Parser(); + this.headers = { + "User-Agent": userAgent, + }; + this.proxyConfig = proxyConfig; + this._accessToken = accessToken; + this._reconnectInterval = 10000; + this._reconnectMaxAttempts = Number.POSITIVE_INFINITY; + this._reconnectCurrentAttempts = 0; + this._connectionClosed = false; + this._client = null; + this._pongReceivedTimestamp = dayjs(); + } - /** - * Start websocket connection. - */ - public start() { - this._connectionClosed = false - this._resetRetryParams() - this._startWebSocketConnection() - } + /** + * Start websocket connection. + */ + public start() { + this._connectionClosed = false; + this._resetRetryParams(); + this._startWebSocketConnection(); + } - /** - * Reset connection and start new websocket connection. - */ - private _startWebSocketConnection() { - this._resetConnection() - this._setupParser() - this._client = this._connect(this.url, this.stream, this.params, this._accessToken, this.headers, this.proxyConfig) - this._bindSocket(this._client) - } + /** + * Reset connection and start new websocket connection. + */ + private _startWebSocketConnection() { + this._resetConnection(); + this._setupParser(); + this._client = this._connect( + this.url, + this.stream, + this.params, + this._accessToken, + this.headers, + this.proxyConfig, + ); + this._bindSocket(this._client); + } - /** - * Stop current connection. - */ - public stop() { - this._connectionClosed = true - this._resetConnection() - this._resetRetryParams() - } + /** + * Stop current connection. + */ + public stop() { + this._connectionClosed = true; + this._resetConnection(); + this._resetRetryParams(); + } - /** - * Clean up current connection, and listeners. - */ - private _resetConnection() { - if (this._client) { - this._client.close(1000) - this._client.removeAllListeners() - this._client = null - } + /** + * Clean up current connection, and listeners. + */ + private _resetConnection() { + if (this._client) { + this._client.close(1000); + this._client.removeAllListeners(); + this._client = null; + } - if (this.parser) { - this.parser.removeAllListeners() - } - } + if (this.parser) { + this.parser.removeAllListeners(); + } + } - /** - * Resets the parameters used in reconnect. - */ - private _resetRetryParams() { - this._reconnectCurrentAttempts = 0 - } + /** + * Resets the parameters used in reconnect. + */ + private _resetRetryParams() { + this._reconnectCurrentAttempts = 0; + } - /** - * Reconnects to the same endpoint. - */ - private _reconnect() { - setTimeout(() => { - // Skip reconnect when client is connecting. - // https://github.com/websockets/ws/blob/7.2.1/lib/websocket.js#L365 - if (this._client && this._client.readyState === WS.CONNECTING) { - return - } + /** + * Reconnects to the same endpoint. + */ + private _reconnect() { + setTimeout(() => { + // Skip reconnect when client is connecting. + // https://github.com/websockets/ws/blob/7.2.1/lib/websocket.js#L365 + if (this._client && this._client.readyState === WS.CONNECTING) { + return; + } - if (this._reconnectCurrentAttempts < this._reconnectMaxAttempts) { - this._reconnectCurrentAttempts++ - this._clearBinding() - if (this._client) { - // In reconnect, we want to close the connection immediately, - // because recoonect is necessary when some problems occur. - this._client.terminate() - } - // Call connect methods - console.log('Reconnecting') - this._client = this._connect(this.url, this.stream, this.params, this._accessToken, this.headers, this.proxyConfig) - this._bindSocket(this._client) - } - }, this._reconnectInterval) - } + if (this._reconnectCurrentAttempts < this._reconnectMaxAttempts) { + this._reconnectCurrentAttempts++; + this._clearBinding(); + if (this._client) { + // In reconnect, we want to close the connection immediately, + // because recoonect is necessary when some problems occur. + this._client.terminate(); + } + // Call connect methods + console.log("Reconnecting"); + this._client = this._connect( + this.url, + this.stream, + this.params, + this._accessToken, + this.headers, + this.proxyConfig, + ); + this._bindSocket(this._client); + } + }, this._reconnectInterval); + } - /** - * @param url Base url of streaming endpoint. - * @param stream The specified stream name. - * @param accessToken Access token. - * @param headers The specified headers. - * @param proxyConfig Proxy setting, or set false if don't use proxy. - * @return A WebSocket instance. - */ - private _connect( - url: string, - stream: string, - params: string | null, - accessToken: string, - headers: { [key: string]: string }, - proxyConfig: ProxyConfig | false - ): WS { - const parameter: Array = [`stream=${stream}`] + /** + * @param url Base url of streaming endpoint. + * @param stream The specified stream name. + * @param accessToken Access token. + * @param headers The specified headers. + * @param proxyConfig Proxy setting, or set false if don't use proxy. + * @return A WebSocket instance. + */ + private _connect( + url: string, + stream: string, + params: string | null, + accessToken: string, + headers: { [key: string]: string }, + proxyConfig: ProxyConfig | false, + ): WS { + const parameter: Array = [`stream=${stream}`]; - if (params) { - parameter.push(params) - } + if (params) { + parameter.push(params); + } - if (accessToken !== null) { - parameter.push(`access_token=${accessToken}`) - } - const requestURL: string = `${url}/?${parameter.join('&')}` - let options: WS.ClientOptions = { - headers: headers - } - if (proxyConfig) { - options = Object.assign(options, { - agent: proxyAgent(proxyConfig) - }) - } + if (accessToken !== null) { + parameter.push(`access_token=${accessToken}`); + } + const requestURL: string = `${url}/?${parameter.join("&")}`; + let options: WS.ClientOptions = { + headers: headers, + }; + if (proxyConfig) { + options = Object.assign(options, { + agent: proxyAgent(proxyConfig), + }); + } - const cli: WS = new WS(requestURL, options) - return cli - } + const cli: WS = new WS(requestURL, options); + return cli; + } - /** - * Clear binding event for web socket client. - */ - private _clearBinding() { - if (this._client) { - this._client.removeAllListeners('close') - this._client.removeAllListeners('pong') - this._client.removeAllListeners('open') - this._client.removeAllListeners('message') - this._client.removeAllListeners('error') - } - } + /** + * Clear binding event for web socket client. + */ + private _clearBinding() { + if (this._client) { + this._client.removeAllListeners("close"); + this._client.removeAllListeners("pong"); + this._client.removeAllListeners("open"); + this._client.removeAllListeners("message"); + this._client.removeAllListeners("error"); + } + } - /** - * Bind event for web socket client. - * @param client A WebSocket instance. - */ - private _bindSocket(client: WS) { - client.on('close', (code: number, _reason: Buffer) => { - // Refer the code: https://tools.ietf.org/html/rfc6455#section-7.4 - if (code === 1000) { - this.emit('close', {}) - } else { - console.log(`Closed connection with ${code}`) - // If already called close method, it does not retry. - if (!this._connectionClosed) { - this._reconnect() - } - } - }) - client.on('pong', () => { - this._pongWaiting = false - this.emit('pong', {}) - this._pongReceivedTimestamp = dayjs() - // It is required to anonymous function since get this scope in checkAlive. - setTimeout(() => this._checkAlive(this._pongReceivedTimestamp), this._heartbeatInterval) - }) - client.on('open', () => { - this.emit('connect', {}) - // Call first ping event. - setTimeout(() => { - client.ping('') - }, 10000) - }) - client.on('message', (data: WS.Data, isBinary: boolean) => { - this.parser.parse(data, isBinary) - }) - client.on('error', (err: Error) => { - this.emit('error', err) - }) - } + /** + * Bind event for web socket client. + * @param client A WebSocket instance. + */ + private _bindSocket(client: WS) { + client.on("close", (code: number, _reason: Buffer) => { + // Refer the code: https://tools.ietf.org/html/rfc6455#section-7.4 + if (code === 1000) { + this.emit("close", {}); + } else { + console.log(`Closed connection with ${code}`); + // If already called close method, it does not retry. + if (!this._connectionClosed) { + this._reconnect(); + } + } + }); + client.on("pong", () => { + this._pongWaiting = false; + this.emit("pong", {}); + this._pongReceivedTimestamp = dayjs(); + // It is required to anonymous function since get this scope in checkAlive. + setTimeout( + () => this._checkAlive(this._pongReceivedTimestamp), + this._heartbeatInterval, + ); + }); + client.on("open", () => { + this.emit("connect", {}); + // Call first ping event. + setTimeout(() => { + client.ping(""); + }, 10000); + }); + client.on("message", (data: WS.Data, isBinary: boolean) => { + this.parser.parse(data, isBinary); + }); + client.on("error", (err: Error) => { + this.emit("error", err); + }); + } - /** - * Set up parser when receive message. - */ - private _setupParser() { - this.parser.on('update', (status: MastodonAPI.Entity.Status) => { - this.emit('update', MastodonAPI.Converter.status(status)) - }) - this.parser.on('notification', (notification: MastodonAPI.Entity.Notification) => { - const n = MastodonAPI.Converter.notification(notification) - if (n instanceof UnknownNotificationTypeError) { - console.warn(`Unknown notification event has received: ${notification}`) - } else { - this.emit('notification', n) - } - }) - this.parser.on('delete', (id: string) => { - this.emit('delete', id) - }) - this.parser.on('conversation', (conversation: MastodonAPI.Entity.Conversation) => { - this.emit('conversation', MastodonAPI.Converter.conversation(conversation)) - }) - this.parser.on('status_update', (status: MastodonAPI.Entity.Status) => { - this.emit('status_update', MastodonAPI.Converter.status(status)) - }) - this.parser.on('error', (err: Error) => { - this.emit('parser-error', err) - }) - this.parser.on('heartbeat', _ => { - this.emit('heartbeat', 'heartbeat') - }) - } + /** + * Set up parser when receive message. + */ + private _setupParser() { + this.parser.on("update", (status: MastodonAPI.Entity.Status) => { + this.emit("update", MastodonAPI.Converter.status(status)); + }); + this.parser.on( + "notification", + (notification: MastodonAPI.Entity.Notification) => { + const n = MastodonAPI.Converter.notification(notification); + if (n instanceof UnknownNotificationTypeError) { + console.warn( + `Unknown notification event has received: ${notification}`, + ); + } else { + this.emit("notification", n); + } + }, + ); + this.parser.on("delete", (id: string) => { + this.emit("delete", id); + }); + this.parser.on( + "conversation", + (conversation: MastodonAPI.Entity.Conversation) => { + this.emit( + "conversation", + MastodonAPI.Converter.conversation(conversation), + ); + }, + ); + this.parser.on("status_update", (status: MastodonAPI.Entity.Status) => { + this.emit("status_update", MastodonAPI.Converter.status(status)); + }); + this.parser.on("error", (err: Error) => { + this.emit("parser-error", err); + }); + this.parser.on("heartbeat", (_) => { + this.emit("heartbeat", "heartbeat"); + }); + } - /** - * Call ping and wait to pong. - */ - private _checkAlive(timestamp: Dayjs) { - const now: Dayjs = dayjs() - // Block multiple calling, if multiple pong event occur. - // It the duration is less than interval, through ping. - if (now.diff(timestamp) > this._heartbeatInterval - 1000 && !this._connectionClosed) { - // Skip ping when client is connecting. - // https://github.com/websockets/ws/blob/7.2.1/lib/websocket.js#L289 - if (this._client && this._client.readyState !== WS.CONNECTING) { - this._pongWaiting = true - this._client.ping('') - setTimeout(() => { - if (this._pongWaiting) { - this._pongWaiting = false - this._reconnect() - } - }, 10000) - } - } - } + /** + * Call ping and wait to pong. + */ + private _checkAlive(timestamp: Dayjs) { + const now: Dayjs = dayjs(); + // Block multiple calling, if multiple pong event occur. + // It the duration is less than interval, through ping. + if ( + now.diff(timestamp) > this._heartbeatInterval - 1000 && + !this._connectionClosed + ) { + // Skip ping when client is connecting. + // https://github.com/websockets/ws/blob/7.2.1/lib/websocket.js#L289 + if (this._client && this._client.readyState !== WS.CONNECTING) { + this._pongWaiting = true; + this._client.ping(""); + setTimeout(() => { + if (this._pongWaiting) { + this._pongWaiting = false; + this._reconnect(); + } + }, 10000); + } + } + } } /** @@ -294,55 +328,60 @@ export default class WebSocket extends EventEmitter implements WebSocketInterfac * This class provides parser for websocket message. */ export class Parser extends EventEmitter { - /** - * @param message Message body of websocket. - */ - public parse(data: WS.Data, isBinary: boolean) { - const message = isBinary ? data : data.toString() - if (typeof message !== 'string') { - this.emit('heartbeat', {}) - return - } + /** + * @param message Message body of websocket. + */ + public parse(data: WS.Data, isBinary: boolean) { + const message = isBinary ? data : data.toString(); + if (typeof message !== "string") { + this.emit("heartbeat", {}); + return; + } - if (message === '') { - this.emit('heartbeat', {}) - return - } + if (message === "") { + this.emit("heartbeat", {}); + return; + } - let event = '' - let payload = '' - let mes = {} - try { - const obj = JSON.parse(message) - event = obj.event - payload = obj.payload - mes = JSON.parse(payload) - } catch (err) { - // delete event does not have json object - if (event !== 'delete') { - this.emit('error', new Error(`Error parsing websocket reply: ${message}, error message: ${err}`)) - return - } - } + let event = ""; + let payload = ""; + let mes = {}; + try { + const obj = JSON.parse(message); + event = obj.event; + payload = obj.payload; + mes = JSON.parse(payload); + } catch (err) { + // delete event does not have json object + if (event !== "delete") { + this.emit( + "error", + new Error( + `Error parsing websocket reply: ${message}, error message: ${err}`, + ), + ); + return; + } + } - switch (event) { - case 'update': - this.emit('update', mes as MastodonAPI.Entity.Status) - break - case 'notification': - this.emit('notification', mes as MastodonAPI.Entity.Notification) - break - case 'conversation': - this.emit('conversation', mes as MastodonAPI.Entity.Conversation) - break - case 'delete': - this.emit('delete', payload) - break - case 'status.update': - this.emit('status_update', mes as MastodonAPI.Entity.Status) - break - default: - this.emit('error', new Error(`Unknown event has received: ${message}`)) - } - } + switch (event) { + case "update": + this.emit("update", mes as MastodonAPI.Entity.Status); + break; + case "notification": + this.emit("notification", mes as MastodonAPI.Entity.Notification); + break; + case "conversation": + this.emit("conversation", mes as MastodonAPI.Entity.Conversation); + break; + case "delete": + this.emit("delete", payload); + break; + case "status.update": + this.emit("status_update", mes as MastodonAPI.Entity.Status); + break; + default: + this.emit("error", new Error(`Unknown event has received: ${message}`)); + } + } }