From afda15260f4f97ec00b3e7fdf63bd13013daae40 Mon Sep 17 00:00:00 2001 From: Mar0xy Date: Sun, 24 Sep 2023 01:44:53 +0200 Subject: upd: megalodon to v7 --- packages/megalodon/src/pleroma/api_client.ts | 823 +++++++++++++++++++++ packages/megalodon/src/pleroma/entities/account.ts | 31 + .../megalodon/src/pleroma/entities/activity.ts | 8 + .../megalodon/src/pleroma/entities/announcement.ts | 39 + .../megalodon/src/pleroma/entities/application.ts | 7 + .../src/pleroma/entities/async_attachment.ts | 14 + .../megalodon/src/pleroma/entities/attachment.ts | 49 ++ packages/megalodon/src/pleroma/entities/card.ts | 11 + packages/megalodon/src/pleroma/entities/context.ts | 8 + .../megalodon/src/pleroma/entities/conversation.ts | 11 + packages/megalodon/src/pleroma/entities/emoji.ts | 8 + .../megalodon/src/pleroma/entities/featured_tag.ts | 8 + packages/megalodon/src/pleroma/entities/field.ts | 7 + packages/megalodon/src/pleroma/entities/filter.ts | 12 + packages/megalodon/src/pleroma/entities/history.ts | 7 + .../src/pleroma/entities/identity_proof.ts | 9 + .../megalodon/src/pleroma/entities/instance.ts | 46 ++ packages/megalodon/src/pleroma/entities/list.ts | 6 + packages/megalodon/src/pleroma/entities/marker.ts | 12 + packages/megalodon/src/pleroma/entities/mention.ts | 8 + .../megalodon/src/pleroma/entities/notification.ts | 16 + packages/megalodon/src/pleroma/entities/poll.ts | 13 + .../megalodon/src/pleroma/entities/poll_option.ts | 6 + .../megalodon/src/pleroma/entities/preferences.ts | 9 + .../src/pleroma/entities/push_subscription.ts | 16 + .../megalodon/src/pleroma/entities/reaction.ts | 10 + .../megalodon/src/pleroma/entities/relationship.ts | 18 + packages/megalodon/src/pleroma/entities/report.ts | 6 + packages/megalodon/src/pleroma/entities/results.ts | 11 + .../src/pleroma/entities/scheduled_status.ts | 10 + packages/megalodon/src/pleroma/entities/source.ts | 10 + packages/megalodon/src/pleroma/entities/stats.ts | 7 + packages/megalodon/src/pleroma/entities/status.ts | 64 ++ .../src/pleroma/entities/status_params.ts | 11 + .../src/pleroma/entities/status_source.ts | 7 + packages/megalodon/src/pleroma/entities/tag.ts | 10 + packages/megalodon/src/pleroma/entities/token.ts | 8 + packages/megalodon/src/pleroma/entities/urls.ts | 5 + packages/megalodon/src/pleroma/entity.ts | 39 + packages/megalodon/src/pleroma/notification.ts | 15 + packages/megalodon/src/pleroma/web_socket.ts | 349 +++++++++ 41 files changed, 1764 insertions(+) create mode 100644 packages/megalodon/src/pleroma/api_client.ts create mode 100644 packages/megalodon/src/pleroma/entities/account.ts create mode 100644 packages/megalodon/src/pleroma/entities/activity.ts create mode 100644 packages/megalodon/src/pleroma/entities/announcement.ts create mode 100644 packages/megalodon/src/pleroma/entities/application.ts create mode 100644 packages/megalodon/src/pleroma/entities/async_attachment.ts create mode 100644 packages/megalodon/src/pleroma/entities/attachment.ts create mode 100644 packages/megalodon/src/pleroma/entities/card.ts create mode 100644 packages/megalodon/src/pleroma/entities/context.ts create mode 100644 packages/megalodon/src/pleroma/entities/conversation.ts create mode 100644 packages/megalodon/src/pleroma/entities/emoji.ts create mode 100644 packages/megalodon/src/pleroma/entities/featured_tag.ts create mode 100644 packages/megalodon/src/pleroma/entities/field.ts create mode 100644 packages/megalodon/src/pleroma/entities/filter.ts create mode 100644 packages/megalodon/src/pleroma/entities/history.ts create mode 100644 packages/megalodon/src/pleroma/entities/identity_proof.ts create mode 100644 packages/megalodon/src/pleroma/entities/instance.ts create mode 100644 packages/megalodon/src/pleroma/entities/list.ts create mode 100644 packages/megalodon/src/pleroma/entities/marker.ts create mode 100644 packages/megalodon/src/pleroma/entities/mention.ts create mode 100644 packages/megalodon/src/pleroma/entities/notification.ts create mode 100644 packages/megalodon/src/pleroma/entities/poll.ts create mode 100644 packages/megalodon/src/pleroma/entities/poll_option.ts create mode 100644 packages/megalodon/src/pleroma/entities/preferences.ts create mode 100644 packages/megalodon/src/pleroma/entities/push_subscription.ts create mode 100644 packages/megalodon/src/pleroma/entities/reaction.ts create mode 100644 packages/megalodon/src/pleroma/entities/relationship.ts create mode 100644 packages/megalodon/src/pleroma/entities/report.ts create mode 100644 packages/megalodon/src/pleroma/entities/results.ts create mode 100644 packages/megalodon/src/pleroma/entities/scheduled_status.ts create mode 100644 packages/megalodon/src/pleroma/entities/source.ts create mode 100644 packages/megalodon/src/pleroma/entities/stats.ts create mode 100644 packages/megalodon/src/pleroma/entities/status.ts create mode 100644 packages/megalodon/src/pleroma/entities/status_params.ts create mode 100644 packages/megalodon/src/pleroma/entities/status_source.ts create mode 100644 packages/megalodon/src/pleroma/entities/tag.ts create mode 100644 packages/megalodon/src/pleroma/entities/token.ts create mode 100644 packages/megalodon/src/pleroma/entities/urls.ts create mode 100644 packages/megalodon/src/pleroma/entity.ts create mode 100644 packages/megalodon/src/pleroma/notification.ts create mode 100644 packages/megalodon/src/pleroma/web_socket.ts (limited to 'packages/megalodon/src/pleroma') diff --git a/packages/megalodon/src/pleroma/api_client.ts b/packages/megalodon/src/pleroma/api_client.ts new file mode 100644 index 0000000000..99d964353e --- /dev/null +++ b/packages/megalodon/src/pleroma/api_client.ts @@ -0,0 +1,823 @@ +import axios, { AxiosResponse, AxiosRequestConfig } from 'axios' +import objectAssignDeep from 'object-assign-deep' + +import MegalodonEntity from '../entity' +import PleromaEntity from './entity' +import Response from '../response' +import { RequestCanceledError } from '../cancel' +import proxyAgent, { ProxyConfig } from '../proxy_config' +import { NO_REDIRECT, DEFAULT_SCOPE, DEFAULT_UA } from '../default' +import WebSocket from './web_socket' +import NotificationType, { UnknownNotificationTypeError } from '../notification' +import PleromaNotificationType from './notification' + +namespace PleromaAPI { + export namespace Entity { + export type Account = PleromaEntity.Account + export type Activity = PleromaEntity.Activity + export type Announcement = PleromaEntity.Announcement + export type Application = PleromaEntity.Application + export type AsyncAttachment = PleromaEntity.AsyncAttachment + export type Attachment = PleromaEntity.Attachment + export type Card = PleromaEntity.Card + export type Context = PleromaEntity.Context + export type Conversation = PleromaEntity.Conversation + export type Emoji = PleromaEntity.Emoji + export type FeaturedTag = PleromaEntity.FeaturedTag + export type Field = PleromaEntity.Field + export type Filter = PleromaEntity.Filter + export type History = PleromaEntity.History + export type IdentityProof = PleromaEntity.IdentityProof + export type Instance = PleromaEntity.Instance + export type List = PleromaEntity.List + export type Marker = PleromaEntity.Marker + export type Mention = PleromaEntity.Mention + export type Notification = PleromaEntity.Notification + export type Poll = PleromaEntity.Poll + export type PollOption = PleromaEntity.PollOption + export type Preferences = PleromaEntity.Preferences + export type PushSubscription = PleromaEntity.PushSubscription + export type Reaction = PleromaEntity.Reaction + export type Relationship = PleromaEntity.Relationship + export type Report = PleromaEntity.Report + export type Results = PleromaEntity.Results + export type ScheduledStatus = PleromaEntity.ScheduledStatus + export type Source = PleromaEntity.Source + export type Stats = PleromaEntity.Stats + export type Status = PleromaEntity.Status + export type StatusParams = PleromaEntity.StatusParams + export type StatusSource = PleromaEntity.StatusSource + export type Tag = PleromaEntity.Tag + export type Token = PleromaEntity.Token + export type URLs = PleromaEntity.URLs + } + + export namespace Converter { + export const decodeNotificationType = ( + t: PleromaEntity.NotificationType + ): MegalodonEntity.NotificationType | UnknownNotificationTypeError => { + switch (t) { + case PleromaNotificationType.Mention: + return NotificationType.Mention + case PleromaNotificationType.Reblog: + return NotificationType.Reblog + case PleromaNotificationType.Favourite: + return NotificationType.Favourite + case PleromaNotificationType.Follow: + return NotificationType.Follow + case PleromaNotificationType.Poll: + return NotificationType.PollExpired + case PleromaNotificationType.PleromaEmojiReaction: + return NotificationType.EmojiReaction + case PleromaNotificationType.FollowRequest: + return NotificationType.FollowRequest + case PleromaNotificationType.Update: + return NotificationType.Update + case PleromaNotificationType.Move: + return NotificationType.Move + default: + return new UnknownNotificationTypeError() + } + } + export const encodeNotificationType = ( + t: MegalodonEntity.NotificationType + ): PleromaEntity.NotificationType | UnknownNotificationTypeError => { + switch (t) { + case NotificationType.Follow: + return PleromaNotificationType.Follow + case NotificationType.Favourite: + return PleromaNotificationType.Favourite + case NotificationType.Reblog: + return PleromaNotificationType.Reblog + case NotificationType.Mention: + return PleromaNotificationType.Mention + case NotificationType.PollExpired: + return PleromaNotificationType.Poll + case NotificationType.EmojiReaction: + return PleromaNotificationType.PleromaEmojiReaction + case NotificationType.FollowRequest: + return PleromaNotificationType.FollowRequest + case NotificationType.Update: + return PleromaNotificationType.Update + case NotificationType.Move: + return PleromaNotificationType.Move + default: + return new UnknownNotificationTypeError() + } + } + + export const account = (a: Entity.Account): MegalodonEntity.Account => { + return { + id: a.id, + username: a.username, + acct: a.acct, + display_name: a.display_name, + locked: a.locked, + discoverable: a.discoverable, + group: null, + noindex: a.noindex, + suspended: a.suspended, + limited: a.limited, + created_at: a.created_at, + followers_count: a.followers_count, + following_count: a.following_count, + statuses_count: a.statuses_count, + note: a.note, + url: a.url, + avatar: a.avatar, + avatar_static: a.avatar_static, + header: a.header, + header_static: a.header_static, + emojis: a.emojis.map(e => emoji(e)), + moved: a.moved ? account(a.moved) : null, + fields: a.fields, + bot: a.bot, + source: a.source + } + } + export const activity = (a: Entity.Activity): MegalodonEntity.Activity => a + export const announcement = (a: Entity.Announcement): MegalodonEntity.Announcement => ({ + id: a.id, + content: a.content, + starts_at: a.starts_at, + ends_at: a.ends_at, + published: a.published, + all_day: a.all_day, + published_at: a.published_at, + updated_at: a.updated_at, + read: null, + mentions: a.mentions, + statuses: a.statuses, + tags: a.tags, + emojis: a.emojis, + reactions: a.reactions + }) + 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 => ({ + url: c.url, + title: c.title, + description: c.description, + type: c.type, + image: c.image, + author_name: null, + author_url: null, + provider_name: c.provider_name, + provider_url: c.provider_url, + html: null, + width: null, + height: null, + embed_url: null, + blurhash: null + }) + 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 => ({ + shortcode: e.shortcode, + static_url: e.static_url, + url: e.url, + visible_in_picker: e.visible_in_picker + }) + export const featured_tag = (f: Entity.FeaturedTag): MegalodonEntity.FeaturedTag => f + 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 => ({ + uri: i.uri, + title: i.title, + description: i.description, + email: i.email, + version: i.version, + thumbnail: i.thumbnail, + urls: urls(i.urls), + stats: stats(i.stats), + languages: i.languages, + registrations: i.registrations, + approval_required: i.approval_required, + configuration: { + statuses: { + max_characters: i.max_toot_chars, + max_media_attachments: i.max_media_attachments + }, + polls: { + max_options: i.poll_limits.max_options, + max_characters_per_option: i.poll_limits.max_option_chars, + min_expiration: i.poll_limits.min_expiration, + max_expiration: i.poll_limits.max_expiration + } + } + }) + export const list = (l: Entity.List): MegalodonEntity.List => ({ + id: l.id, + title: l.title, + replies_policy: null + }) + export const marker = (m: Entity.Marker | Record): MegalodonEntity.Marker | Record => { + if ((m as any).notifications) { + const mm = m as Entity.Marker + return { + notifications: { + last_read_id: mm.notifications.last_read_id, + version: mm.notifications.version, + updated_at: mm.notifications.updated_at, + unread_count: mm.notifications.pleroma.unread_count + } + } + } else { + return {} + } + } + 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 && n.emoji) { + return { + id: n.id, + account: account(n.account), + created_at: n.created_at, + status: status(n.status), + emoji: n.emoji, + type: notificationType + } + } else if (n.status) { + return { + id: n.id, + account: account(n.account), + created_at: n.created_at, + status: status(n.status), + type: notificationType + } + } else if (n.target) { + return { + id: n.id, + account: account(n.account), + created_at: n.created_at, + target: account(n.target), + type: notificationType + } + } else { + return { + id: n.id, + account: account(n.account), + created_at: n.created_at, + type: notificationType + } + } + } + export const poll = (p: Entity.Poll): MegalodonEntity.Poll => p + export const pollOption = (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 reaction = (r: Entity.Reaction): MegalodonEntity.Reaction => { + const p = { + count: r.count, + me: r.me, + name: r.name + } + if (r.accounts) { + return Object.assign({}, p, { + accounts: r.accounts.map(a => account(a)) + }) + } + return p + } + export const relationship = (r: Entity.Relationship): MegalodonEntity.Relationship => ({ + id: r.id, + following: r.following, + followed_by: r.followed_by, + blocking: r.blocking, + blocked_by: r.blocked_by, + muting: r.muting, + muting_notifications: r.muting_notifications, + requested: r.requested, + domain_blocking: r.domain_blocking, + showing_reblogs: r.showing_reblogs, + endorsed: r.endorsed, + notifying: r.notifying, + note: r.note + }) + export const report = (r: Entity.Report): MegalodonEntity.Report => ({ + id: r.id, + action_taken: r.action_taken, + action_taken_at: null, + category: null, + comment: null, + forwarded: null, + status_ids: null, + rule_ids: null + }) + 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 => ({ + id: s.id, + scheduled_at: s.scheduled_at, + params: status_params(s.params), + media_attachments: Array.isArray(s.media_attachments) ? s.media_attachments.map(m => attachment(m)) : null + }) + 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) : null, + content: s.content, + plain_content: s.pleroma.content?.['text/plain'] ? s.pleroma.content['text/plain'] : 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: Array.isArray(s.pleroma.emoji_reactions) ? s.pleroma.emoji_reactions.map(r => reaction(r)) : [], + bookmarked: s.bookmarked ? s.bookmarked : false, + quote: s.reblog !== null && s.reblog.content !== s.content + }) + export const status_params = (s: Entity.StatusParams): MegalodonEntity.StatusParams => { + return { + text: s.text, + in_reply_to_id: s.in_reply_to_id, + media_ids: Array.isArray(s.media_ids) ? s.media_ids : null, + sensitive: s.sensitive, + spoiler_text: s.spoiler_text, + visibility: s.visibility, + scheduled_at: s.scheduled_at, + application_id: null + } + } + 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 + } + + /** + * Interface + */ + export interface Interface { + get(path: string, params?: any, headers?: { [key: string]: string }): 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://pleroma.io' + static NO_REDIRECT = NO_REDIRECT + + 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 + } + + /** + * 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 } = {}): Promise> { + let options: AxiosRequestConfig = { + params: params, + headers: headers + } + 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(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: Infinity, + maxBodyLength: 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: Infinity, + maxBodyLength: 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: Infinity, + maxBodyLength: 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: Infinity, + maxBodyLength: 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: Infinity, + maxBodyLength: 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: Infinity, + maxBodyLength: 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: Infinity, + maxBodyLength: 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() + } + + /** + * 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 default PleromaAPI diff --git a/packages/megalodon/src/pleroma/entities/account.ts b/packages/megalodon/src/pleroma/entities/account.ts new file mode 100644 index 0000000000..29d42643fc --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/account.ts @@ -0,0 +1,31 @@ +/// +/// +/// +namespace PleromaEntity { + export type Account = { + id: string + username: string + acct: string + display_name: string + locked: boolean + discoverable?: boolean + noindex: boolean | null + suspended: boolean | null + limited: boolean | null + created_at: string + followers_count: number + following_count: number + statuses_count: number + note: string + url: string + avatar: string + avatar_static: string + header: string + header_static: string + emojis: Array + moved: Account | null + fields: Array + bot: boolean + source?: Source + } +} diff --git a/packages/megalodon/src/pleroma/entities/activity.ts b/packages/megalodon/src/pleroma/entities/activity.ts new file mode 100644 index 0000000000..f70ad168eb --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/activity.ts @@ -0,0 +1,8 @@ +namespace PleromaEntity { + export type Activity = { + week: string + statuses: string + logins: string + registrations: string + } +} diff --git a/packages/megalodon/src/pleroma/entities/announcement.ts b/packages/megalodon/src/pleroma/entities/announcement.ts new file mode 100644 index 0000000000..247ad90c5b --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/announcement.ts @@ -0,0 +1,39 @@ +/// + +namespace PleromaEntity { + export type Announcement = { + id: string + content: string + starts_at: string | null + ends_at: string | null + published: boolean + all_day: boolean + published_at: string + updated_at: string + mentions: Array + statuses: Array + tags: Array + emojis: Array + reactions: Array + } + + export type AnnouncementAccount = { + id: string + username: string + url: string + acct: string + } + + export type AnnouncementStatus = { + id: string + url: string + } + + export type AnnouncementReaction = { + name: string + count: number + me: boolean | null + url: string | null + static_url: string | null + } +} diff --git a/packages/megalodon/src/pleroma/entities/application.ts b/packages/megalodon/src/pleroma/entities/application.ts new file mode 100644 index 0000000000..055592d6ce --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/application.ts @@ -0,0 +1,7 @@ +namespace PleromaEntity { + export type Application = { + name: string + website?: string | null + vapid_key?: string | null + } +} diff --git a/packages/megalodon/src/pleroma/entities/async_attachment.ts b/packages/megalodon/src/pleroma/entities/async_attachment.ts new file mode 100644 index 0000000000..8784979cbb --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/async_attachment.ts @@ -0,0 +1,14 @@ +/// +namespace PleromaEntity { + export type AsyncAttachment = { + id: string + type: 'unknown' | 'image' | 'gifv' | 'video' | 'audio' + url: string | null + remote_url: string | null + preview_url: string + text_url: string | null + meta: Meta | null + description: string | null + blurhash: string | null + } +} diff --git a/packages/megalodon/src/pleroma/entities/attachment.ts b/packages/megalodon/src/pleroma/entities/attachment.ts new file mode 100644 index 0000000000..18d4371daf --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/attachment.ts @@ -0,0 +1,49 @@ +namespace PleromaEntity { + export type Sub = { + // For Image, Gifv, and Video + width?: number + height?: number + size?: string + aspect?: number + + // For Gifv and Video + frame_rate?: string + + // For Audio, Gifv, and Video + duration?: number + bitrate?: number + } + + export type Focus = { + x: number + y: number + } + + export type Meta = { + original?: Sub + small?: Sub + focus?: Focus + length?: string + duration?: number + fps?: number + size?: string + width?: number + height?: number + aspect?: number + audio_encode?: string + audio_bitrate?: string + audio_channel?: string + } + + export type Attachment = { + id: string + type: 'unknown' | 'image' | 'gifv' | 'video' | 'audio' + url: string + remote_url: string | null + preview_url: string | null + text_url: string | null + meta: Meta | null + description: string | null + blurhash: string | null + } +} diff --git a/packages/megalodon/src/pleroma/entities/card.ts b/packages/megalodon/src/pleroma/entities/card.ts new file mode 100644 index 0000000000..9aca99a8c8 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/card.ts @@ -0,0 +1,11 @@ +namespace PleromaEntity { + export type Card = { + url: string + title: string + description: string + type: 'link' | 'photo' | 'video' | 'rich' + image: string | null + provider_name: string + provider_url: string + } +} diff --git a/packages/megalodon/src/pleroma/entities/context.ts b/packages/megalodon/src/pleroma/entities/context.ts new file mode 100644 index 0000000000..f297bd2c17 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/context.ts @@ -0,0 +1,8 @@ +/// + +namespace PleromaEntity { + export type Context = { + ancestors: Array + descendants: Array + } +} diff --git a/packages/megalodon/src/pleroma/entities/conversation.ts b/packages/megalodon/src/pleroma/entities/conversation.ts new file mode 100644 index 0000000000..624e6da389 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/conversation.ts @@ -0,0 +1,11 @@ +/// +/// + +namespace PleromaEntity { + export type Conversation = { + id: string + accounts: Array + last_status: Status | null + unread: boolean + } +} diff --git a/packages/megalodon/src/pleroma/entities/emoji.ts b/packages/megalodon/src/pleroma/entities/emoji.ts new file mode 100644 index 0000000000..43ea22d770 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/emoji.ts @@ -0,0 +1,8 @@ +namespace PleromaEntity { + export type Emoji = { + shortcode: string + static_url: string + url: string + visible_in_picker: boolean + } +} diff --git a/packages/megalodon/src/pleroma/entities/featured_tag.ts b/packages/megalodon/src/pleroma/entities/featured_tag.ts new file mode 100644 index 0000000000..a42e27f9d0 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/featured_tag.ts @@ -0,0 +1,8 @@ +namespace PleromaEntity { + export type FeaturedTag = { + id: string + name: string + statuses_count: number + last_status_at: string + } +} diff --git a/packages/megalodon/src/pleroma/entities/field.ts b/packages/megalodon/src/pleroma/entities/field.ts new file mode 100644 index 0000000000..01803078a9 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/field.ts @@ -0,0 +1,7 @@ +namespace PleromaEntity { + export type Field = { + name: string + value: string + verified_at: string | null + } +} diff --git a/packages/megalodon/src/pleroma/entities/filter.ts b/packages/megalodon/src/pleroma/entities/filter.ts new file mode 100644 index 0000000000..08a18089c2 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/filter.ts @@ -0,0 +1,12 @@ +namespace PleromaEntity { + export type Filter = { + id: string + phrase: string + context: Array + expires_at: string | null + irreversible: boolean + whole_word: boolean + } + + export type FilterContext = string +} diff --git a/packages/megalodon/src/pleroma/entities/history.ts b/packages/megalodon/src/pleroma/entities/history.ts new file mode 100644 index 0000000000..9aaaeb8def --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/history.ts @@ -0,0 +1,7 @@ +namespace PleromaEntity { + export type History = { + day: string + uses: number + accounts: number + } +} diff --git a/packages/megalodon/src/pleroma/entities/identity_proof.ts b/packages/megalodon/src/pleroma/entities/identity_proof.ts new file mode 100644 index 0000000000..463fdc6817 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/identity_proof.ts @@ -0,0 +1,9 @@ +namespace PleromaEntity { + export type IdentityProof = { + provider: string + provider_username: string + updated_at: string + proof_url: string + profile_url: string + } +} diff --git a/packages/megalodon/src/pleroma/entities/instance.ts b/packages/megalodon/src/pleroma/entities/instance.ts new file mode 100644 index 0000000000..0b57e805e9 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/instance.ts @@ -0,0 +1,46 @@ +/// +/// +/// + +namespace PleromaEntity { + export type Instance = { + uri: string + title: string + description: string + email: string + version: string + thumbnail: string | null + urls: URLs + stats: Stats + languages: Array + registrations: boolean + approval_required: boolean + max_toot_chars: number + max_media_attachments?: number + pleroma: { + metadata: { + account_activation_required: boolean + birthday_min_age: number + birthday_required: boolean + features: Array + federation: { + enabled: boolean + exclusions: boolean + } + fields_limits: { + max_fields: number + max_remote_fields: number + name_length: number + value_length: number + } + post_formats: Array + } + } + poll_limits: { + max_expiration: number + min_expiration: number + max_option_chars: number + max_options: number + } + } +} diff --git a/packages/megalodon/src/pleroma/entities/list.ts b/packages/megalodon/src/pleroma/entities/list.ts new file mode 100644 index 0000000000..a3d4362d9e --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/list.ts @@ -0,0 +1,6 @@ +namespace PleromaEntity { + export type List = { + id: string + title: string + } +} diff --git a/packages/megalodon/src/pleroma/entities/marker.ts b/packages/megalodon/src/pleroma/entities/marker.ts new file mode 100644 index 0000000000..720d4a9055 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/marker.ts @@ -0,0 +1,12 @@ +namespace PleromaEntity { + export type Marker = { + notifications: { + last_read_id: string + version: number + updated_at: string + pleroma: { + unread_count: number + } + } + } +} diff --git a/packages/megalodon/src/pleroma/entities/mention.ts b/packages/megalodon/src/pleroma/entities/mention.ts new file mode 100644 index 0000000000..0d68b4ec21 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/mention.ts @@ -0,0 +1,8 @@ +namespace PleromaEntity { + export type Mention = { + id: string + username: string + url: string + acct: string + } +} diff --git a/packages/megalodon/src/pleroma/entities/notification.ts b/packages/megalodon/src/pleroma/entities/notification.ts new file mode 100644 index 0000000000..edfa456deb --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/notification.ts @@ -0,0 +1,16 @@ +/// +/// + +namespace PleromaEntity { + export type Notification = { + account: Account + created_at: string + id: string + status?: Status + emoji?: string + type: NotificationType + target?: Account + } + + export type NotificationType = string +} diff --git a/packages/megalodon/src/pleroma/entities/poll.ts b/packages/megalodon/src/pleroma/entities/poll.ts new file mode 100644 index 0000000000..82e0182adc --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/poll.ts @@ -0,0 +1,13 @@ +/// + +namespace PleromaEntity { + export type Poll = { + id: string + expires_at: string | null + expired: boolean + multiple: boolean + votes_count: number + options: Array + voted: boolean + } +} diff --git a/packages/megalodon/src/pleroma/entities/poll_option.ts b/packages/megalodon/src/pleroma/entities/poll_option.ts new file mode 100644 index 0000000000..69717ca0f3 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/poll_option.ts @@ -0,0 +1,6 @@ +namespace PleromaEntity { + export type PollOption = { + title: string + votes_count: number | null + } +} diff --git a/packages/megalodon/src/pleroma/entities/preferences.ts b/packages/megalodon/src/pleroma/entities/preferences.ts new file mode 100644 index 0000000000..99f8d6bca1 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/preferences.ts @@ -0,0 +1,9 @@ +namespace PleromaEntity { + export type Preferences = { + 'posting:default:visibility': 'public' | 'unlisted' | 'private' | 'direct' + 'posting:default:sensitive': boolean + 'posting:default:language': string | null + 'reading:expand:media': 'default' | 'show_all' | 'hide_all' + 'reading:expand:spoilers': boolean + } +} diff --git a/packages/megalodon/src/pleroma/entities/push_subscription.ts b/packages/megalodon/src/pleroma/entities/push_subscription.ts new file mode 100644 index 0000000000..b3e14e68a3 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/push_subscription.ts @@ -0,0 +1,16 @@ +namespace PleromaEntity { + export type Alerts = { + follow: boolean + favourite: boolean + mention: boolean + reblog: boolean + poll: boolean + } + + export type PushSubscription = { + id: string + endpoint: string + server_key: string + alerts: Alerts + } +} diff --git a/packages/megalodon/src/pleroma/entities/reaction.ts b/packages/megalodon/src/pleroma/entities/reaction.ts new file mode 100644 index 0000000000..662600f252 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/reaction.ts @@ -0,0 +1,10 @@ +/// + +namespace PleromaEntity { + export type Reaction = { + count: number + me: boolean + name: string + accounts?: Array + } +} diff --git a/packages/megalodon/src/pleroma/entities/relationship.ts b/packages/megalodon/src/pleroma/entities/relationship.ts new file mode 100644 index 0000000000..039f8ec74b --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/relationship.ts @@ -0,0 +1,18 @@ +namespace PleromaEntity { + export type Relationship = { + id: string + following: boolean + followed_by: boolean + blocking: boolean + blocked_by: boolean + muting: boolean + muting_notifications: boolean + requested: boolean + domain_blocking: boolean + showing_reblogs: boolean + endorsed: boolean + subscribing: boolean + notifying: boolean + note: string + } +} diff --git a/packages/megalodon/src/pleroma/entities/report.ts b/packages/megalodon/src/pleroma/entities/report.ts new file mode 100644 index 0000000000..5b9c650a16 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/report.ts @@ -0,0 +1,6 @@ +namespace PleromaEntity { + export type Report = { + id: string + action_taken: boolean + } +} diff --git a/packages/megalodon/src/pleroma/entities/results.ts b/packages/megalodon/src/pleroma/entities/results.ts new file mode 100644 index 0000000000..cd42e3b090 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/results.ts @@ -0,0 +1,11 @@ +/// +/// +/// + +namespace PleromaEntity { + export type Results = { + accounts: Array + statuses: Array + hashtags: Array + } +} diff --git a/packages/megalodon/src/pleroma/entities/scheduled_status.ts b/packages/megalodon/src/pleroma/entities/scheduled_status.ts new file mode 100644 index 0000000000..547d35fd8f --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/scheduled_status.ts @@ -0,0 +1,10 @@ +/// +/// +namespace PleromaEntity { + export type ScheduledStatus = { + id: string + scheduled_at: string + params: StatusParams + media_attachments: Array | null + } +} diff --git a/packages/megalodon/src/pleroma/entities/source.ts b/packages/megalodon/src/pleroma/entities/source.ts new file mode 100644 index 0000000000..f2fa74ab70 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/source.ts @@ -0,0 +1,10 @@ +/// +namespace PleromaEntity { + export type Source = { + privacy: string | null + sensitive: boolean | null + language: string | null + note: string + fields: Array + } +} diff --git a/packages/megalodon/src/pleroma/entities/stats.ts b/packages/megalodon/src/pleroma/entities/stats.ts new file mode 100644 index 0000000000..ab3e778454 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/stats.ts @@ -0,0 +1,7 @@ +namespace PleromaEntity { + export type Stats = { + user_count: number + status_count: number + domain_count: number + } +} diff --git a/packages/megalodon/src/pleroma/entities/status.ts b/packages/megalodon/src/pleroma/entities/status.ts new file mode 100644 index 0000000000..1949ec954c --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/status.ts @@ -0,0 +1,64 @@ +/// +/// +/// +/// +/// +/// +/// +/// + +namespace PleromaEntity { + export type Status = { + id: string + uri: string + url: string + account: Account + in_reply_to_id: string | null + in_reply_to_account_id: string | null + reblog: Status | null + content: string + created_at: string + emojis: Emoji[] + replies_count: number + reblogs_count: number + favourites_count: number + reblogged: boolean | null + favourited: boolean | null + muted: boolean | null + sensitive: boolean + spoiler_text: string + visibility: 'public' | 'unlisted' | 'private' | 'direct' + media_attachments: Array + mentions: Array + tags: Array + card: Card | null + poll: Poll | null + application: Application | null + language: string | null + pinned: boolean | null + bookmarked?: boolean + // Reblogged status contains only local parameter. + pleroma: { + content?: { + 'text/plain': string + } + spoiler_text?: { + 'text/plain': string + } + conversation_id?: number + direct_conversation_id?: number | null + emoji_reactions?: Array + expires_at?: string + in_reply_to_account_acct?: string + local: boolean + parent_visible?: boolean + pinned_at?: string + thread_muted?: boolean + } + } + + export type StatusTag = { + name: string + url: string + } +} diff --git a/packages/megalodon/src/pleroma/entities/status_params.ts b/packages/megalodon/src/pleroma/entities/status_params.ts new file mode 100644 index 0000000000..eda13a0b9b --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/status_params.ts @@ -0,0 +1,11 @@ +namespace PleromaEntity { + export type StatusParams = { + text: string + in_reply_to_id: string | null + media_ids?: Array | null + sensitive: boolean | null + spoiler_text: string | null + visibility: 'public' | 'unlisted' | 'private' | 'direct' | null + scheduled_at: string | null + } +} diff --git a/packages/megalodon/src/pleroma/entities/status_source.ts b/packages/megalodon/src/pleroma/entities/status_source.ts new file mode 100644 index 0000000000..57d2bea781 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/status_source.ts @@ -0,0 +1,7 @@ +namespace PleromaEntity { + export type StatusSource = { + id: string + text: string + spoiler_text: string + } +} diff --git a/packages/megalodon/src/pleroma/entities/tag.ts b/packages/megalodon/src/pleroma/entities/tag.ts new file mode 100644 index 0000000000..e323ec72c3 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/tag.ts @@ -0,0 +1,10 @@ +/// + +namespace PleromaEntity { + export type Tag = { + name: string + url: string + history: Array + following?: boolean + } +} diff --git a/packages/megalodon/src/pleroma/entities/token.ts b/packages/megalodon/src/pleroma/entities/token.ts new file mode 100644 index 0000000000..0ac565b517 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/token.ts @@ -0,0 +1,8 @@ +namespace PleromaEntity { + export type Token = { + access_token: string + token_type: string + scope: string + created_at: number + } +} diff --git a/packages/megalodon/src/pleroma/entities/urls.ts b/packages/megalodon/src/pleroma/entities/urls.ts new file mode 100644 index 0000000000..7ad6faf2b0 --- /dev/null +++ b/packages/megalodon/src/pleroma/entities/urls.ts @@ -0,0 +1,5 @@ +namespace PleromaEntity { + export type URLs = { + streaming_api: string + } +} diff --git a/packages/megalodon/src/pleroma/entity.ts b/packages/megalodon/src/pleroma/entity.ts new file mode 100644 index 0000000000..bd486f62bd --- /dev/null +++ b/packages/megalodon/src/pleroma/entity.ts @@ -0,0 +1,39 @@ +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// > +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// + +export default PleromaEntity diff --git a/packages/megalodon/src/pleroma/notification.ts b/packages/megalodon/src/pleroma/notification.ts new file mode 100644 index 0000000000..2dad51a6e3 --- /dev/null +++ b/packages/megalodon/src/pleroma/notification.ts @@ -0,0 +1,15 @@ +import PleromaEntity from './entity' + +namespace PleromaNotificationType { + export const Mention: PleromaEntity.NotificationType = 'mention' + export const Reblog: PleromaEntity.NotificationType = 'reblog' + export const Favourite: PleromaEntity.NotificationType = 'favourite' + export const Follow: PleromaEntity.NotificationType = 'follow' + export const Poll: PleromaEntity.NotificationType = 'poll' + export const PleromaEmojiReaction: PleromaEntity.NotificationType = 'pleroma:emoji_reaction' + export const FollowRequest: PleromaEntity.NotificationType = 'follow_request' + export const Update: PleromaEntity.NotificationType = 'update' + export const Move: PleromaEntity.NotificationType = 'move' +} + +export default PleromaNotificationType diff --git a/packages/megalodon/src/pleroma/web_socket.ts b/packages/megalodon/src/pleroma/web_socket.ts new file mode 100644 index 0000000000..f96ea5dc56 --- /dev/null +++ b/packages/megalodon/src/pleroma/web_socket.ts @@ -0,0 +1,349 @@ +import WS from 'ws' +import dayjs, { Dayjs } from 'dayjs' +import { EventEmitter } from 'events' + +import proxyAgent, { ProxyConfig } from '../proxy_config' +import { WebSocketInterface } from '../megalodon' +import PleromaAPI 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: number = 60000 + private _pongWaiting: boolean = 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 = 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() + } + + /** + * 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() + } + + /** + * 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 + } + + /** + * 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) + } + + /** + * @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 (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 + } + + /** + * 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) + }) + } + + /** + * Set up parser when receive message. + */ + private _setupParser() { + this.parser.on('update', (status: PleromaAPI.Entity.Status) => { + this.emit('update', PleromaAPI.Converter.status(status)) + }) + this.parser.on('notification', (notification: PleromaAPI.Entity.Notification) => { + const n = PleromaAPI.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: PleromaAPI.Entity.Conversation) => { + this.emit('conversation', PleromaAPI.Converter.conversation(conversation)) + }) + this.parser.on('status_update', (status: PleromaAPI.Entity.Status) => { + this.emit('status_update', PleromaAPI.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) + } + } + } +} + +/** + * Parser + * 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 + } + + 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 + } + } + + switch (event) { + case 'update': + this.emit('update', mes as PleromaAPI.Entity.Status) + break + case 'notification': + this.emit('notification', mes as PleromaAPI.Entity.Notification) + break + case 'conversation': + this.emit('conversation', mes as PleromaAPI.Entity.Conversation) + break + case 'delete': + this.emit('delete', payload) + break + case 'status.update': + this.emit('status_update', mes as PleromaAPI.Entity.Status) + break + default: + this.emit('error', new Error(`Unknown event has received: ${message}`)) + } + } +} -- cgit v1.2.3-freya