diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/backend/src/server/oauth/OAuth2ProviderService.ts | 2 | ||||
| -rw-r--r-- | packages/megalodon/package.json | 17 | ||||
| -rw-r--r-- | packages/megalodon/src/detector.ts | 8 | ||||
| -rw-r--r-- | packages/megalodon/src/index.ts | 5 | ||||
| -rw-r--r-- | packages/megalodon/src/megalodon.ts | 20 | ||||
| -rw-r--r-- | packages/megalodon/src/misskey.ts | 33 | ||||
| -rw-r--r-- | packages/megalodon/src/misskey/api_client.ts | 43 | ||||
| -rw-r--r-- | packages/megalodon/src/misskey/web_socket.ts | 413 | ||||
| -rw-r--r-- | packages/megalodon/src/proxy_config.ts | 50 |
9 files changed, 8 insertions, 583 deletions
diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index b7e09633ed..a65acb7c9b 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -156,7 +156,7 @@ export class OAuth2ProviderService { const secret = String(body.client_secret); const code = body.code ? String(body.code) : ''; - // TODO fetch the access token directly + // TODO fetch the access token directly, then remove all oauth code from megalodon const client = this.mastodonClientService.getClient(request); const atData = await client.fetchAccessToken(clientId, secret, code); diff --git a/packages/megalodon/package.json b/packages/megalodon/package.json index 1ceb47759d..f10a9cf9dc 100644 --- a/packages/megalodon/package.json +++ b/packages/megalodon/package.json @@ -54,25 +54,13 @@ }, "homepage": "https://github.com/h3poteto/megalodon#readme", "dependencies": { - "@types/core-js": "^2.5.8", - "@types/form-data": "^2.5.0", "@types/jest": "^29.5.10", "@types/oauth": "^0.9.4", - "@types/object-assign-deep": "^0.4.3", - "@types/parse-link-header": "^2.0.3", - "@types/uuid": "^9.0.7", - "@types/ws": "^8.5.10", "axios": "1.7.4", "dayjs": "^1.11.10", "form-data": "4.0.2", - "https-proxy-agent": "^7.0.2", "oauth": "0.10.2", - "object-assign-deep": "^0.4.0", - "parse-link-header": "^2.0.0", - "socks-proxy-agent": "^8.0.2", - "typescript": "5.8.3", - "uuid": "11.1.0", - "ws": "8.17.1" + "typescript": "5.8.3" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "8.31.0", @@ -80,8 +68,7 @@ "eslint": "9.25.1", "eslint-config-prettier": "^9.0.0", "jest": "29.7.0", - "jest-worker": "^29.7.0", - "lodash": "4.17.21", + "jest-worker": "29.7.0", "prettier": "3.5.3", "ts-jest": "^29.1.1" } diff --git a/packages/megalodon/src/detector.ts b/packages/megalodon/src/detector.ts index 31f34d72f7..ba95d96816 100644 --- a/packages/megalodon/src/detector.ts +++ b/packages/megalodon/src/detector.ts @@ -1,5 +1,4 @@ import axios, { AxiosRequestConfig } from 'axios' -import proxyAgent, { ProxyConfig } from './proxy_config' import { NodeinfoError } from './megalodon' const NODEINFO_10 = 'http://nodeinfo.diaspora.software/ns/schema/1.0' @@ -45,21 +44,14 @@ type Metadata = { * Now support Mastodon, Pleroma and Pixelfed. Throws an error when no known platform can be detected. * * @param url Base URL of SNS. - * @param proxyConfig Proxy setting, or set false if don't use proxy. * @return SNS name. */ export const detector = async ( url: string, - proxyConfig: ProxyConfig | false = false ): Promise<'mastodon' | 'pleroma' | 'misskey' | 'friendica'> => { let options: AxiosRequestConfig = { timeout: 20000 } - if (proxyConfig) { - options = Object.assign(options, { - httpsAgent: proxyAgent(proxyConfig) - }) - } const res = await axios.get<Links>(url + '/.well-known/nodeinfo', options) const link = res.data.links.find(l => l.rel === NODEINFO_20 || l.rel === NODEINFO_21) diff --git a/packages/megalodon/src/index.ts b/packages/megalodon/src/index.ts index 7a4f10ab02..50663c3ce5 100644 --- a/packages/megalodon/src/index.ts +++ b/packages/megalodon/src/index.ts @@ -1,8 +1,7 @@ import Response from './response' import OAuth from './oauth' import { isCancel, RequestCanceledError } from './cancel' -import { ProxyConfig } from './proxy_config' -import { MegalodonInterface, WebSocketInterface } from './megalodon' +import { MegalodonInterface } from './megalodon' import { detector } from './detector' import Misskey from './misskey' import Entity from './entity' @@ -16,10 +15,8 @@ export { OAuth, RequestCanceledError, isCancel, - ProxyConfig, detector, MegalodonInterface, - WebSocketInterface, NotificationType, FilterContext, Misskey, diff --git a/packages/megalodon/src/megalodon.ts b/packages/megalodon/src/megalodon.ts index 6032c351c9..4257e545df 100644 --- a/packages/megalodon/src/megalodon.ts +++ b/packages/megalodon/src/megalodon.ts @@ -2,16 +2,6 @@ import Response from './response' import OAuth from './oauth' import Entity from './entity' -export interface WebSocketInterface { - start(): void - stop(): void - // EventEmitter - on(event: string | symbol, listener: (...args: any[]) => void): this - once(event: string | symbol, listener: (...args: any[]) => void): this - removeListener(event: string | symbol, listener: (...args: any[]) => void): this - removeAllListeners(event?: string | symbol): this -} - export interface MegalodonInterface { /** * Cancel all requests in this instance. @@ -1361,16 +1351,6 @@ export interface MegalodonInterface { * @return Reaction. **/ getEmojiReaction(id: string, emoji: string): Promise<Response<Entity.Reaction>> - - // ====================================== - // WebSocket - // ====================================== - userSocket(): WebSocketInterface - publicSocket(): WebSocketInterface - localSocket(): WebSocketInterface - tagSocket(tag: string): WebSocketInterface - listSocket(list_id: string): WebSocketInterface - directSocket(): WebSocketInterface } export class NoImplementedError extends Error { diff --git a/packages/megalodon/src/misskey.ts b/packages/megalodon/src/misskey.ts index eb1e5824b8..670b31e838 100644 --- a/packages/megalodon/src/misskey.ts +++ b/packages/megalodon/src/misskey.ts @@ -2,28 +2,24 @@ import FormData from 'form-data' import fs from 'fs'; import MisskeyAPI from './misskey/api_client' import { DEFAULT_UA } from './default' -import { ProxyConfig } from './proxy_config' import OAuth from './oauth' import Response from './response' -import { MegalodonInterface, WebSocketInterface, NoImplementedError, ArgumentError, UnexpectedError } from './megalodon' +import { MegalodonInterface, NoImplementedError, ArgumentError, UnexpectedError } from './megalodon' import { UnknownNotificationTypeError } from './notification' export default class Misskey implements MegalodonInterface { public client: MisskeyAPI.Interface public baseUrl: string - public proxyConfig: ProxyConfig | 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 | null = DEFAULT_UA, - proxyConfig: ProxyConfig | false = false ) { let token: string = '' if (accessToken) { @@ -33,9 +29,8 @@ export default class Misskey implements MegalodonInterface { if (userAgent) { agent = userAgent } - this.client = new MisskeyAPI.Client(baseUrl, token, agent, proxyConfig) + this.client = new MisskeyAPI.Client(baseUrl, token, agent) this.baseUrl = baseUrl - this.proxyConfig = proxyConfig } public cancel(): void { @@ -2562,28 +2557,4 @@ export default class Misskey implements MegalodonInterface { reject(err) }) } - - public userSocket(): WebSocketInterface { - return this.client.socket('user') - } - - public publicSocket(): WebSocketInterface { - return this.client.socket('globalTimeline') - } - - public localSocket(): WebSocketInterface { - return this.client.socket('localTimeline') - } - - public tagSocket(_tag: string): WebSocketInterface { - throw new NoImplementedError('TODO: implement') - } - - public listSocket(list_id: string): WebSocketInterface { - return this.client.socket('list', list_id) - } - - public directSocket(): WebSocketInterface { - return this.client.socket('conversation') - } } diff --git a/packages/megalodon/src/misskey/api_client.ts b/packages/megalodon/src/misskey/api_client.ts index 4d97ae497c..17a59c7c76 100644 --- a/packages/megalodon/src/misskey/api_client.ts +++ b/packages/megalodon/src/misskey/api_client.ts @@ -3,11 +3,9 @@ import dayjs from 'dayjs' import FormData from 'form-data' import { DEFAULT_UA } from '../default' -import proxyAgent, { ProxyConfig } from '../proxy_config' import Response from '../response' import MisskeyEntity from './entity' import MegalodonEntity from '../entity' -import WebSocket from './web_socket' import MisskeyNotificationType from './notification' import * as NotificationType from '../notification' import { UnknownNotificationTypeError } from '../notification'; @@ -555,32 +553,28 @@ namespace MisskeyAPI { get<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>> post<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>> cancel(): void - socket(channel: 'user' | 'localTimeline' | 'hybridTimeline' | 'globalTimeline' | 'conversation' | 'list', listId?: string): WebSocket } /** * Misskey API client. * - * Usign axios for request, you will handle promises. + * Using axios for request, you will handle promises. */ export class Client implements Interface { 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, userAgent: string = DEFAULT_UA, proxyConfig: ProxyConfig | false = false) { + constructor(baseUrl: string, accessToken: string | null, userAgent: string = DEFAULT_UA) { this.accessToken = accessToken this.baseUrl = baseUrl this.userAgent = userAgent - this.proxyConfig = proxyConfig this.abortController = new AbortController() axios.defaults.signal = this.abortController.signal } @@ -595,12 +589,6 @@ namespace MisskeyAPI { maxContentLength: Infinity, maxBodyLength: Infinity } - if (this.proxyConfig) { - options = Object.assign(options, { - httpAgent: proxyAgent(this.proxyConfig), - httpsAgent: proxyAgent(this.proxyConfig) - }) - } return axios.get<T>(this.baseUrl + path, options).then((resp: AxiosResponse<T>) => { const res: Response<T> = { data: resp.data, @@ -624,12 +612,6 @@ namespace MisskeyAPI { maxContentLength: Infinity, maxBodyLength: Infinity } - if (this.proxyConfig) { - options = Object.assign(options, { - httpAgent: proxyAgent(this.proxyConfig), - httpsAgent: proxyAgent(this.proxyConfig) - }) - } let bodyParams = params if (this.accessToken) { if (params instanceof FormData) { @@ -658,27 +640,6 @@ namespace MisskeyAPI { public cancel(): void { return this.abortController.abort() } - - /** - * Get connection and receive websocket connection for Misskey API. - * - * @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list. - * @param listId This parameter is required only list channel. - */ - public socket( - channel: 'user' | 'localTimeline' | 'hybridTimeline' | 'globalTimeline' | 'conversation' | 'list', - listId?: string - ): WebSocket { - if (!this.accessToken) { - throw new Error('accessToken is required') - } - const url = this.baseUrl + '/streaming' - const streaming = new WebSocket(url, channel, this.accessToken, listId, this.userAgent, this.proxyConfig) - process.nextTick(() => { - streaming.start() - }) - return streaming - } } } diff --git a/packages/megalodon/src/misskey/web_socket.ts b/packages/megalodon/src/misskey/web_socket.ts deleted file mode 100644 index 181fb1c903..0000000000 --- a/packages/megalodon/src/misskey/web_socket.ts +++ /dev/null @@ -1,413 +0,0 @@ -import WS from 'ws' -import dayjs, { Dayjs } from 'dayjs' -import { v4 as uuid } from 'uuid' -import { EventEmitter } from 'events' -import { WebSocketInterface } from '../megalodon' -import proxyAgent, { ProxyConfig } from '../proxy_config' -import MisskeyAPI from './api_client' -import { UnknownNotificationTypeError } from '../notification' - -/** - * WebSocket - * Misskey is not support http streaming. It supports websocket instead of streaming. - * So this class connect to Misskey server with WebSocket. - */ -export default class WebSocket extends EventEmitter implements WebSocketInterface { - public url: string - public channel: 'user' | 'localTimeline' | 'hybridTimeline' | 'globalTimeline' | 'conversation' | 'list' - public parser: Parser - public headers: { [key: string]: string } - public proxyConfig: ProxyConfig | false = false - public listId: string | null = null - private _accessToken: string - private _reconnectInterval: number - private _reconnectMaxAttempts: number - private _reconnectCurrentAttempts: number - private _connectionClosed: boolean - private _client: WS | null = null - private _channelID: string - private _pongReceivedTimestamp: Dayjs - private _heartbeatInterval: number = 60000 - private _pongWaiting: boolean = false - - /** - * @param url Full url of websocket: e.g. wss://misskey.io/streaming - * @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list. - * @param accessToken The access token. - * @param listId This parameter is required when you specify list as channel. - */ - constructor( - url: string, - channel: 'user' | 'localTimeline' | 'hybridTimeline' | 'globalTimeline' | 'conversation' | 'list', - accessToken: string, - listId: string | undefined, - userAgent: string, - proxyConfig: ProxyConfig | false = false - ) { - super() - this.url = url - this.parser = new Parser() - this.channel = channel - this.headers = { - 'User-Agent': userAgent - } - if (listId === undefined) { - this.listId = null - } else { - this.listId = listId - } - this.proxyConfig = proxyConfig - this._accessToken = accessToken - this._reconnectInterval = 10000 - this._reconnectMaxAttempts = Infinity - this._reconnectCurrentAttempts = 0 - this._connectionClosed = false - this._channelID = uuid() - this._pongReceivedTimestamp = dayjs() - } - - /** - * 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._bindSocket(this._client) - } - - /** - * 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 - } - - if (this.parser) { - this.parser.removeAllListeners() - } - } - - /** - * Resets the parameters used in reconnect. - */ - private _resetRetryParams() { - this._reconnectCurrentAttempts = 0 - } - - /** - * Connect to the endpoint. - */ - private _connect(): WS { - let options: WS.ClientOptions = { - headers: this.headers - } - if (this.proxyConfig) { - options = Object.assign(options, { - agent: proxyAgent(this.proxyConfig) - }) - } - const cli: WS = new WS(`${this.url}?i=${this._accessToken}`, options) - return cli - } - - /** - * Connect specified channels in websocket. - */ - private _channel() { - if (!this._client) { - return - } - switch (this.channel) { - case 'conversation': - this._client.send( - JSON.stringify({ - type: 'connect', - body: { - channel: 'main', - id: this._channelID - } - }) - ) - break - case 'user': - this._client.send( - JSON.stringify({ - type: 'connect', - body: { - channel: 'main', - id: this._channelID - } - }) - ) - this._client.send( - JSON.stringify({ - type: 'connect', - body: { - channel: 'homeTimeline', - id: this._channelID - } - }) - ) - break - case 'list': - this._client.send( - JSON.stringify({ - type: 'connect', - body: { - channel: 'userList', - id: this._channelID, - params: { - listId: this.listId - } - } - }) - ) - break - default: - this._client.send( - JSON.stringify({ - type: 'connect', - body: { - channel: this.channel, - id: this._channelID - } - }) - ) - break - } - } - - /** - * 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._bindSocket(this._client) - } - }, this._reconnectInterval) - } - - /** - * Clear binding event for websocket 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) => { - if (code === 1000) { - this.emit('close', {}) - } else { - console.log(`Closed connection with ${code}`) - 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', {}) - this._channel() - // Call first ping event. - setTimeout(() => { - client.ping('') - }, 10000) - }) - client.on('message', (data: WS.Data, isBinary: boolean) => { - this.parser.parse(data, isBinary, this._channelID) - }) - client.on('error', (err: Error) => { - this.emit('error', err) - }) - } - - /** - * Set up parser when receive message. - */ - private _setupParser() { - this.parser.on('update', (note: MisskeyAPI.Entity.Note) => { - this.emit('update', MisskeyAPI.Converter.note(note)) - }) - this.parser.on('notification', (notification: MisskeyAPI.Entity.Notification) => { - const n = MisskeyAPI.Converter.notification(notification) - if (n instanceof UnknownNotificationTypeError) { - console.warn(`Unknown notification event has received: ${notification}`) - } else { - this.emit('notification', n) - } - }) - this.parser.on('conversation', (note: MisskeyAPI.Entity.Note) => { - this.emit('conversation', MisskeyAPI.Converter.noteToConversation(note)) - }) - this.parser.on('error', (err: Error) => { - this.emit('parser-error', err) - }) - } - - /** - * 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) - } - } - } -} - -/** - * Parser - * This class provides parser for websocket message. - */ -export class Parser extends EventEmitter { - /** - * @param message Message body of websocket. - * @param channelID Parse only messages which has same channelID. - */ - public parse(data: WS.Data, isBinary: boolean, channelID: string) { - const message = isBinary ? data : data.toString() - if (typeof message !== 'string') { - this.emit('heartbeat', {}) - return - } - - if (message === '') { - this.emit('heartbeat', {}) - return - } - - let obj: { - type: string - body: { - id: string - type: string - body: any - } - } - let body: { - id: string - type: string - body: any - } - - try { - obj = JSON.parse(message) - if (obj.type !== 'channel') { - return - } - if (!obj.body) { - return - } - body = obj.body - if (body.id !== channelID) { - return - } - } catch (err) { - this.emit('error', new Error(`Error parsing websocket reply: ${message}, error message: ${err}`)) - return - } - - switch (body.type) { - case 'note': - this.emit('update', body.body as MisskeyAPI.Entity.Note) - break - case 'notification': - this.emit('notification', body.body as MisskeyAPI.Entity.Notification) - break - case 'mention': { - const note = body.body as MisskeyAPI.Entity.Note - if (note.visibility === 'specified') { - this.emit('conversation', note) - } - break - } - // When renote and followed event, the same notification will be received. - case 'renote': - case 'followed': - case 'follow': - case 'unfollow': - case 'receiveFollowRequest': - case 'meUpdated': - case 'readAllNotifications': - case 'readAllUnreadSpecifiedNotes': - case 'readAllAntennas': - case 'readAllUnreadMentions': - case 'unreadNotification': - // Ignore these events - break - default: - this.emit('error', new Error(`Unknown event has received: ${JSON.stringify(body)}`)) - break - } - } -} diff --git a/packages/megalodon/src/proxy_config.ts b/packages/megalodon/src/proxy_config.ts deleted file mode 100644 index c9ae01b736..0000000000 --- a/packages/megalodon/src/proxy_config.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { HttpsProxyAgent } from 'https-proxy-agent' -import { SocksProxyAgent } from 'socks-proxy-agent' - -export type ProxyConfig = { - host: string - port: number - auth?: { - username: string - password: string - } - protocol: 'http' | 'https' | 'socks4' | 'socks4a' | 'socks5' | 'socks5h' | 'socks' -} - -class ProxyProtocolError extends Error {} - -const proxyAgent = (proxyConfig: ProxyConfig): HttpsProxyAgent<'http'> | HttpsProxyAgent<'https'> | SocksProxyAgent => { - switch (proxyConfig.protocol) { - case 'http': { - let url = new URL(`http://${proxyConfig.host}:${proxyConfig.port}`) - if (proxyConfig.auth) { - url = new URL(`http://${proxyConfig.auth.username}:${proxyConfig.auth.password}@${proxyConfig.host}:${proxyConfig.port}`) - } - const httpsAgent = new HttpsProxyAgent<'http'>(url) - return httpsAgent - } - case 'https': { - let url = new URL(`https://${proxyConfig.host}:${proxyConfig.port}`) - if (proxyConfig.auth) { - url = new URL(`https://${proxyConfig.auth.username}:${proxyConfig.auth.password}@${proxyConfig.host}:${proxyConfig.port}`) - } - const httpsAgent = new HttpsProxyAgent<'https'>(url) - return httpsAgent - } - case 'socks4': - case 'socks4a': - case 'socks5': - case 'socks5h': - case 'socks': { - let url = `socks://${proxyConfig.host}:${proxyConfig.port}` - if (proxyConfig.auth) { - url = `socks://${proxyConfig.auth.username}:${proxyConfig.auth.password}@${proxyConfig.host}:${proxyConfig.port}` - } - const socksAgent = new SocksProxyAgent(url) - return socksAgent - } - default: - throw new ProxyProtocolError('protocol is not accepted') - } -} -export default proxyAgent |