import { OAuth2 } from 'oauth' import FormData from 'form-data' import parseLinkHeader from 'parse-link-header' import MastodonAPI from './mastodon/api_client' import WebSocket from './mastodon/web_socket' import { MegalodonInterface, NoImplementedError } from './megalodon' import Response from './response' import Entity from './entity' import { NO_REDIRECT, DEFAULT_SCOPE, DEFAULT_UA } from './default' import { ProxyConfig } from './proxy_config' import OAuth from './oauth' import { UnknownNotificationTypeError } from './notification' export default class Mastodon implements MegalodonInterface { public client: MastodonAPI.Interface public baseUrl: string /** * @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) { token = accessToken } let agent: string = DEFAULT_UA if (userAgent) { agent = userAgent } this.client = new MastodonAPI.Client(baseUrl, token, agent, proxyConfig) this.baseUrl = baseUrl } public cancel(): void { return this.client.cancel() } /** * Call /api/v1/apps * * Create an application. * @param client_name your application's name * @param options Form Data */ public async registerApp( client_name: string, options: Partial<{ scopes: Array; redirect_uris: string; website: string }> ): Promise { const scopes = options.scopes || DEFAULT_SCOPE return this.createApp(client_name, options).then(async appData => { return this.generateAuthUrl(appData.client_id, appData.client_secret, { scope: scopes, redirect_uri: appData.redirect_uri }).then(url => { appData.url = url return appData }) }) } /** * Call /api/v1/apps * * Create an application. * @param client_name your application's name * @param options Form Data */ public async createApp( client_name: string, options: Partial<{ scopes: Array; redirect_uris: string; website: string }> ): Promise { const scopes = options.scopes || DEFAULT_SCOPE const redirect_uris = options.redirect_uris || NO_REDIRECT const params: { client_name: string redirect_uris: string scopes: string website?: string } = { client_name: client_name, redirect_uris: redirect_uris, scopes: scopes.join(' ') } if (options.website) params.website = options.website return this.client .post('/api/v1/apps', params) .then((res: Response) => OAuth.AppData.from(res.data)) } /** * Generate authorization url using OAuth2. * * @param clientId your OAuth app's client ID * @param clientSecret your OAuth app's client Secret * @param options as property, redirect_uri and scope are available, and must be the same as when you register your app */ public generateAuthUrl( clientId: string, clientSecret: string, options: Partial<{ scope: Array; redirect_uri: string }> ): Promise { const scope = options.scope || DEFAULT_SCOPE const redirect_uri = options.redirect_uri || NO_REDIRECT return new Promise(resolve => { const oauth = new OAuth2(clientId, clientSecret, this.baseUrl, undefined, '/oauth/token') const url = oauth.getAuthorizeUrl({ redirect_uri: redirect_uri, response_type: 'code', client_id: clientId, scope: scope.join(' ') }) resolve(url) }) } // ====================================== // apps // ====================================== /** * GET /api/v1/apps/verify_credentials * * @return An Application */ public verifyAppCredentials(): Promise> { return this.client.get('/api/v1/apps/verify_credentials') } // ====================================== // apps/oauth // ====================================== /** * POST /oauth/token * * Fetch OAuth access token. * Get an access token based client_id and client_secret and authorization code. * @param client_id will be generated by #createApp or #registerApp * @param client_secret will be generated by #createApp or #registerApp * @param code will be generated by the link of #generateAuthUrl or #registerApp * @param redirect_uri must be the same uri as the time when you register your OAuth application */ public async fetchAccessToken( client_id: string | null, client_secret: string, code: string, redirect_uri: string = NO_REDIRECT ): Promise { if (!client_id) { throw new Error('client_id is required') } return this.client .post('/oauth/token', { client_id, client_secret, code, redirect_uri, grant_type: 'authorization_code' }) .then((res: Response) => OAuth.TokenData.from(res.data)) } /** * POST /oauth/revoke * * Revoke an OAuth token. * @param client_id will be generated by #createApp or #registerApp * @param client_secret will be generated by #createApp or #registerApp * @param token will be get #fetchAccessToken */ public async refreshToken(client_id: string, client_secret: string, refresh_token: string): Promise { return this.client .post('/oauth/token', { client_id, client_secret, refresh_token, grant_type: 'refresh_token' }) .then((res: Response) => OAuth.TokenData.from(res.data)) } /** * POST /oauth/revoke * * Revoke an OAuth token. * @param client_id will be generated by #createApp or #registerApp * @param client_secret will be generated by #createApp or #registerApp * @param token will be get #fetchAccessToken */ public async revokeToken(client_id: string, client_secret: string, token: string): Promise> { return this.client.post<{}>('/oauth/revoke', { client_id, client_secret, token }) } // ====================================== // accounts // ====================================== /** * POST /api/v1/accounts * * @param username Username for the account. * @param email Email for the account. * @param password Password for the account. * @param agreement Whether the user agrees to the local rules, terms, and policies. * @param locale The language of the confirmation email that will be sent * @param reason Text that will be reviewed by moderators if registrations require manual approval. * @return An account token. */ public async registerAccount( username: string, email: string, password: string, agreement: boolean, locale: string, reason?: string | null ): Promise> { let params = { username: username, email: email, password: password, agreement: agreement, locale: locale } if (reason) { params = Object.assign(params, { reason: reason }) } return this.client.post('/api/v1/accounts', params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.token(res.data) }) }) } /** * GET /api/v1/accounts/verify_credentials * * @return Account. */ public async verifyAccountCredentials(): Promise> { return this.client.get('/api/v1/accounts/verify_credentials').then(res => { return Object.assign(res, { data: MastodonAPI.Converter.account(res.data) }) }) } /** * PATCH /api/v1/accounts/update_credentials * * @return An account. */ public async updateCredentials(options?: { discoverable?: boolean bot?: boolean display_name?: string note?: string avatar?: string header?: string locked?: boolean source?: { privacy?: string sensitive?: boolean language?: string } fields_attributes?: Array<{ name: string; value: string }> }): Promise> { let params = {} if (options) { if (options.discoverable !== undefined) { params = Object.assign(params, { discoverable: options.discoverable }) } if (options.bot !== undefined) { params = Object.assign(params, { bot: options.bot }) } if (options.display_name) { params = Object.assign(params, { display_name: options.display_name }) } if (options.note) { params = Object.assign(params, { note: options.note }) } if (options.avatar) { params = Object.assign(params, { avatar: options.avatar }) } if (options.header) { params = Object.assign(params, { header: options.header }) } if (options.locked !== undefined) { params = Object.assign(params, { locked: options.locked }) } if (options.source) { params = Object.assign(params, { source: options.source }) } if (options.fields_attributes) { params = Object.assign(params, { fields_attributes: options.fields_attributes }) } } return this.client.patch('/api/v1/accounts/update_credentials', params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.account(res.data) }) }) } /** * GET /api/v1/accounts/:id * * @param id The account ID. * @return An account. */ public async getAccount(id: string): Promise> { return this.client.get(`/api/v1/accounts/${id}`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.account(res.data) }) }) } /** * GET /api/v1/accounts/:id/statuses * * @param id The account ID. * @param options.limit Max number of results to return. Defaults to 20. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID but starting with most recent. * @param options.min_id Return results newer than ID. * @param options.pinned Return statuses which include pinned statuses. * @param options.exclude_replies Return statuses which exclude replies. * @param options.exclude_reblogs Return statuses which exclude reblogs. * @param options.only_media Show only statuses with media attached? Defaults to false. * @return Account's statuses. */ public async getAccountStatuses( id: string, options?: { limit?: number max_id?: string since_id?: string min_id?: string pinned?: boolean exclude_replies?: boolean exclude_reblogs?: boolean only_media: boolean } ): Promise>> { let params = {} if (options) { if (options.limit) { params = Object.assign(params, { limit: options.limit }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.pinned) { params = Object.assign(params, { pinned: options.pinned }) } if (options.exclude_replies) { params = Object.assign(params, { exclude_replies: options.exclude_replies }) } if (options.exclude_reblogs) { params = Object.assign(params, { exclude_reblogs: options.exclude_reblogs }) } if (options.only_media) { params = Object.assign(params, { only_media: options.only_media }) } } return this.client.get>(`/api/v1/accounts/${id}/statuses`, params).then(res => { return Object.assign(res, { data: res.data.map(s => MastodonAPI.Converter.status(s)) }) }) } public getAccountFavourites( _id: string, _options?: { limit?: number max_id?: string since_id?: string } ): Promise>> { return new Promise((_, reject) => { const err = new NoImplementedError('mastodon does not support') reject(err) }) } /** * POST /api/v1/accounts/:id/follow * * @param id Target account ID. * @return Relationship. */ public async subscribeAccount(id: string): Promise> { const params = { notify: true } return this.client.post(`/api/v1/accounts/${id}/follow`, params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data) }) }) } /** * POST /api/v1/accounts/:id/follow * * @param id Target account ID. * @return Relationship. */ public async unsubscribeAccount(id: string): Promise> { const params = { notify: false } return this.client.post(`/api/v1/accounts/${id}/follow`, params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data) }) }) } /** * GET /api/v1/accounts/:id/followers * * @param id The account ID. * @param options.limit Max number of results to return. Defaults to 40. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @return The array of accounts. */ public async getAccountFollowers( id: string, options?: { limit?: number max_id?: string since_id?: string get_all?: boolean sleep_ms?: number } ): Promise>> { let params = {} if (options) { if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.urlToAccounts(`/api/v1/accounts/${id}/followers`, params, options?.get_all || false, options?.sleep_ms || 0) } /** * GET /api/v1/accounts/:id/following * * @param id The account ID. * @param options.limit Max number of results to return. Defaults to 40. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @return The array of accounts. */ public async getAccountFollowing( id: string, options?: { limit?: number max_id?: string since_id?: string get_all?: boolean sleep_ms?: number } ): Promise>> { let params = {} if (options) { if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.urlToAccounts(`/api/v1/accounts/${id}/following`, params, options?.get_all || false, options?.sleep_ms || 0) } /** Helper function to optionally follow Link headers as pagination */ private async urlToAccounts(url: string, params: Record, get_all: boolean, sleep_ms: number) { const res = await this.client.get>(url, params) let converted = Object.assign({}, res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) if (get_all && converted.headers.link) { let parsed = parseLinkHeader(converted.headers.link) while (parsed?.next) { const nextRes = await this.client.get>(parsed?.next.url, undefined, undefined, true) converted = Object.assign({}, converted, { data: [...converted.data, ...nextRes.data.map(a => MastodonAPI.Converter.account(a))] }) parsed = parseLinkHeader(nextRes.headers.link) if (sleep_ms) { await new Promise(converted => setTimeout(converted, sleep_ms)) } } } return converted } /** * GET /api/v1/accounts/:id/lists * * @param id The account ID. * @return The array of lists. */ public async getAccountLists(id: string): Promise>> { return this.client.get>(`/api/v1/accounts/${id}/lists`).then(res => { return Object.assign(res, { data: res.data.map(l => MastodonAPI.Converter.list(l)) }) }) } /** * GET /api/v1/accounts/:id/identity_proofs * * @param id The account ID. * @return Array of IdentityProof */ public async getIdentityProof(id: string): Promise>> { return this.client.get>(`/api/v1/accounts/${id}/identity_proofs`).then(res => { return Object.assign(res, { data: res.data.map(i => MastodonAPI.Converter.identity_proof(i)) }) }) } /** * POST /api/v1/accounts/:id/follow * * @param id The account ID. * @param reblog Receive this account's reblogs in home timeline. * @return Relationship */ public async followAccount(id: string, options?: { reblog?: boolean }): Promise> { let params = {} if (options) { if (options.reblog !== undefined) { params = Object.assign(params, { reblog: options.reblog }) } } return this.client.post(`/api/v1/accounts/${id}/follow`, params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data) }) }) } /** * POST /api/v1/accounts/:id/unfollow * * @param id The account ID. * @return Relationship */ public async unfollowAccount(id: string): Promise> { return this.client.post(`/api/v1/accounts/${id}/unfollow`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data) }) }) } /** * POST /api/v1/accounts/:id/block * * @param id The account ID. * @return Relationship */ public async blockAccount(id: string): Promise> { return this.client.post(`/api/v1/accounts/${id}/block`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data) }) }) } /** * POST /api/v1/accounts/:id/unblock * * @param id The account ID. * @return RElationship */ public async unblockAccount(id: string): Promise> { return this.client.post(`/api/v1/accounts/${id}/unblock`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data) }) }) } /** * POST /api/v1/accounts/:id/mute * * @param id The account ID. * @param notifications Mute notifications in addition to statuses. * @return Relationship */ public async muteAccount(id: string, notifications: boolean = true): Promise> { return this.client .post(`/api/v1/accounts/${id}/mute`, { notifications: notifications }) .then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data) }) }) } /** * POST /api/v1/accounts/:id/unmute * * @param id The account ID. * @return Relationship */ public async unmuteAccount(id: string): Promise> { return this.client.post(`/api/v1/accounts/${id}/unmute`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data) }) }) } /** * POST /api/v1/accounts/:id/pin * * @param id The account ID. * @return Relationship */ public async pinAccount(id: string): Promise> { return this.client.post(`/api/v1/accounts/${id}/pin`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data) }) }) } /** * POST /api/v1/accounts/:id/unpin * * @param id The account ID. * @return Relationship */ public async unpinAccount(id: string): Promise> { return this.client.post(`/api/v1/accounts/${id}/unpin`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data) }) }) } /** * GET /api/v1/accounts/relationships * * @param id The account ID. * @return Relationship */ public async getRelationship(id: string): Promise> { return this.client .get>('/api/v1/accounts/relationships', { id: [id] }) .then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data[0]) }) }) } /** * GET /api/v1/accounts/relationships * * @param ids Array of account IDs. * @return Array of Relationship. */ public async getRelationships(ids: Array): Promise>> { return this.client .get>('/api/v1/accounts/relationships', { id: ids }) .then(res => { return Object.assign(res, { data: res.data.map(r => MastodonAPI.Converter.relationship(r)) }) }) } /** * GET /api/v1/accounts/search * * @param q Search query. * @param options.limit Max number of results to return. Defaults to 40. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @return The array of accounts. */ public async searchAccount( q: string, options?: { following?: boolean resolve?: boolean limit?: number max_id?: string since_id?: string } ): Promise>> { let params = { q: q } if (options) { if (options.following !== undefined && options.following !== null) { params = Object.assign(params, { following: options.following }) } if (options.resolve !== undefined && options.resolve !== null) { params = Object.assign(params, { resolve: options.resolve }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.client.get>('/api/v1/accounts/search', params).then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) }) } // ====================================== // accounts/bookmarks // ====================================== /** * GET /api/v1/bookmarks * * @param options.limit Max number of results to return. Defaults to 40. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of statuses. */ public async getBookmarks(options?: { limit?: number max_id?: string since_id?: string min_id?: string }): Promise>> { let params = {} if (options) { if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } } return this.client.get>('/api/v1/bookmarks', params).then(res => { return Object.assign(res, { data: res.data.map(s => MastodonAPI.Converter.status(s)) }) }) } // ====================================== // accounts/favourites // ====================================== /** * GET /api/v1/favourites * * @param options.limit Max number of results to return. Defaults to 40. * @param options.max_id Return results older than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of statuses. */ public async getFavourites(options?: { limit?: number; max_id?: string; min_id?: string }): Promise>> { let params = {} if (options) { if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.client.get>('/api/v1/favourites', params).then(res => { return Object.assign(res, { data: res.data.map(s => MastodonAPI.Converter.status(s)) }) }) } // ====================================== // accounts/mutes // ====================================== /** * GET /api/v1/mutes * * @param options.limit Max number of results to return. Defaults to 40. * @param options.max_id Return results older than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of accounts. */ public async getMutes(options?: { limit?: number; max_id?: string; min_id?: string }): Promise>> { let params = {} if (options) { if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.client.get>('/api/v1/mutes', params).then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) }) } // ====================================== // accounts/blocks // ====================================== /** * GET /api/v1/blocks * * @param options.limit Max number of results to return. Defaults to 40. * @param options.max_id Return results older than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of accounts. */ public async getBlocks(options?: { limit?: number; max_id?: string; min_id?: string }): Promise>> { let params = {} if (options) { if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.client.get>('/api/v1/blocks', params).then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) }) } // ====================================== // accounts/domain_blocks // ====================================== /** * GET /api/v1/domain_blocks * * @param options.limit Max number of results to return. Defaults to 40. * @param options.max_id Return results older than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of domain name. */ public async getDomainBlocks(options?: { limit?: number; max_id?: string; min_id?: string }): Promise>> { let params = {} if (options) { if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.client.get>('/api/v1/domain_blocks', params) } /** * POST/api/v1/domain_blocks * * @param domain Domain to block. */ public blockDomain(domain: string): Promise> { return this.client.post<{}>('/api/v1/domain_blocks', { domain: domain }) } /** * DELETE /api/v1/domain_blocks * * @param domain Domain to unblock */ public unblockDomain(domain: string): Promise> { return this.client.del<{}>('/api/v1/domain_blocks', { domain: domain }) } // ====================================== // accounts/filters // ====================================== /** * GET /api/v1/filters * * @return Array of filters. */ public async getFilters(): Promise>> { return this.client.get>('/api/v1/filters').then(res => { return Object.assign(res, { data: res.data.map(f => MastodonAPI.Converter.filter(f)) }) }) } /** * GET /api/v1/filters/:id * * @param id The filter ID. * @return Filter. */ public async getFilter(id: string): Promise> { return this.client.get(`/api/v1/filters/${id}`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.filter(res.data) }) }) } /** * POST /api/v1/filters * * @param phrase Text to be filtered. * @param context Array of enumerable strings home, notifications, public, thread, account. At least one context must be specified. * @param options.irreversible Should the server irreversibly drop matching entities from home and notifications? * @param options.whole_word Consider word boundaries? * @param options.expires_in ISO 8601 Datetime for when the filter expires. * @return Filter */ public async createFilter( phrase: string, context: Array, options?: { irreversible?: boolean whole_word?: boolean expires_in?: string } ): Promise> { let params = { phrase: phrase, context: context } if (options) { if (options.irreversible !== undefined) { params = Object.assign(params, { irreversible: options.irreversible }) } if (options.whole_word !== undefined) { params = Object.assign(params, { whole_word: options.whole_word }) } if (options.expires_in) { params = Object.assign(params, { expires_in: options.expires_in }) } } return this.client.post('/api/v1/filters', params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.filter(res.data) }) }) } /** * PUT /api/v1/filters/:id * * @param id The filter ID. * @param phrase Text to be filtered. * @param context Array of enumerable strings home, notifications, public, thread, account. At least one context must be specified. * @param options.irreversible Should the server irreversibly drop matching entities from home and notifications? * @param options.whole_word Consider word boundaries? * @param options.expires_in ISO 8601 Datetime for when the filter expires. * @return Filter */ public async updateFilter( id: string, phrase: string, context: Array, options?: { irreversible?: boolean whole_word?: boolean expires_in?: string } ): Promise> { let params = { phrase: phrase, context: context } if (options) { if (options.irreversible !== undefined) { params = Object.assign(params, { irreversible: options.irreversible }) } if (options.whole_word !== undefined) { params = Object.assign(params, { whole_word: options.whole_word }) } if (options.expires_in) { params = Object.assign(params, { expires_in: options.expires_in }) } } return this.client.put(`/api/v1/filters/${id}`, params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.filter(res.data) }) }) } /** * DELETE /api/v1/filters/:id * * @param id The filter ID. * @return Removed filter. */ public async deleteFilter(id: string): Promise> { return this.client.del(`/api/v1/filters/${id}`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.filter(res.data) }) }) } // ====================================== // accounts/reports // ====================================== /** * POST /api/v1/reports * * @param account_id Target account ID. * @param options.status_ids Array of Statuses ids to attach to the report. * @param options.comment The reason for the report. Default maximum of 1000 characters. * @param options.forward If the account is remote, should the report be forwarded to the remote admin? * @param options.category Specify if the report is due to spam, violation of enumerated instance rules, or some other reason. Defaults to other. Will be set to violation if rule_ids[] is provided (regardless of any category value you provide). * @param options.rule_ids For violation category reports, specify the ID of the exact rules broken. Rules and their IDs are available via GET /api/v1/instance/rules and GET /api/v1/instance. * @return Report */ public async report( account_id: string, options?: { status_ids?: Array comment: string forward?: boolean category?: Entity.Category rule_ids?: Array } ): Promise> { let params = { account_id: account_id } if (options) { if (options.status_ids) { params = Object.assign(params, { status_ids: options.status_ids }) } if (options.comment) { params = Object.assign(params, { comment: options.comment }) } if (options.forward !== undefined) { params = Object.assign(params, { forward: options.forward }) } if (options.category) { params = Object.assign(params, { category: options.category }) } if (options.rule_ids) { params = Object.assign(params, { rule_ids: options.rule_ids }) } } return this.client.post('/api/v1/reports', params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.report(res.data) }) }) } // ====================================== // accounts/follow_requests // ====================================== /** * GET /api/v1/follow_requests * * @param limit Maximum number of results. * @return Array of account. */ public async getFollowRequests(limit?: number): Promise>> { if (limit) { return this.client .get>('/api/v1/follow_requests', { limit: limit }) .then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) }) } else { return this.client.get>('/api/v1/follow_requests').then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) }) } } /** * POST /api/v1/follow_requests/:id/authorize * * @param id Target account ID. * @return Relationship. */ public async acceptFollowRequest(id: string): Promise> { return this.client.post(`/api/v1/follow_requests/${id}/authorize`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data) }) }) } /** * POST /api/v1/follow_requests/:id/reject * * @param id Target account ID. * @return Relationship. */ public async rejectFollowRequest(id: string): Promise> { return this.client.post(`/api/v1/follow_requests/${id}/reject`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.relationship(res.data) }) }) } // ====================================== // accounts/endorsements // ====================================== /** * GET /api/v1/endorsements * * @param options.limit Max number of results to return. Defaults to 40. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @return Array of accounts. */ public async getEndorsements(options?: { limit?: number; max_id?: string; since_id?: string }): Promise>> { let params = {} if (options) { if (options.limit) { params = Object.assign(params, { limit: options.limit }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } } return this.client.get>('/api/v1/endorsements', params).then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) }) } // ====================================== // accounts/featured_tags // ====================================== /** * GET /api/v1/featured_tags * * @return Array of featured tag. */ public async getFeaturedTags(): Promise>> { return this.client.get>('/api/v1/featured_tags').then(res => { return Object.assign(res, { data: res.data.map(f => MastodonAPI.Converter.featured_tag(f)) }) }) } /** * POST /api/v1/featured_tags * * @param name Target hashtag name. * @return FeaturedTag. */ public async createFeaturedTag(name: string): Promise> { return this.client .post('/api/v1/featured_tags', { name: name }) .then(res => { return Object.assign(res, { data: MastodonAPI.Converter.featured_tag(res.data) }) }) } /** * DELETE /api/v1/featured_tags/:id * * @param id Target featured tag id. * @return Empty */ public deleteFeaturedTag(id: string): Promise> { return this.client.del<{}>(`/api/v1/featured_tags/${id}`) } /** * GET /api/v1/featured_tags/suggestions * * @return Array of tag. */ public async getSuggestedTags(): Promise>> { return this.client.get>('/api/v1/featured_tags/suggestions').then(res => { return Object.assign(res, { data: res.data.map(t => MastodonAPI.Converter.tag(t)) }) }) } // ====================================== // accounts/preferences // ====================================== /** * GET /api/v1/preferences * * @return Preferences. */ public async getPreferences(): Promise> { return this.client.get('/api/v1/preferences').then(res => { return Object.assign(res, { data: MastodonAPI.Converter.preferences(res.data) }) }) } // ====================================== // accounts/followed_tags // ====================================== /** * GET /api/v1/followed_tags * * @return Array of Tag. */ public async getFollowedTags(): Promise>> { return this.client.get>('/api/v1/followed_tags').then(res => { return Object.assign(res, { data: res.data.map(tag => MastodonAPI.Converter.tag(tag)) }) }) } // ====================================== // accounts/suggestions // ====================================== /** * GET /api/v1/suggestions * * @param limit Maximum number of results. * @return Array of accounts. */ public async getSuggestions(limit?: number): Promise>> { if (limit) { return this.client .get>('/api/v1/suggestions', { limit: limit }) .then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) }) } else { return this.client.get>('/api/v1/suggestions').then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) }) } } // ====================================== // accounts/tags // ====================================== /** * GET /api/v1/tags/:id * * @param id Target hashtag id. * @return Tag */ public async getTag(id: string): Promise> { return this.client.get(`/api/v1/tags/${id}`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.tag(res.data) }) }) } /** * POST /api/v1/tags/:id/follow * * @param id Target hashtag id. * @return Tag */ public async followTag(id: string): Promise> { return this.client.post(`/api/v1/tags/${id}/follow`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.tag(res.data) }) }) } /** * POST /api/v1/tags/:id/unfollow * * @param id Target hashtag id. * @return Tag */ public async unfollowTag(id: string): Promise> { return this.client.post(`/api/v1/tags/${id}/unfollow`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.tag(res.data) }) }) } // ====================================== // statuses // ====================================== /** * POST /api/v1/statuses * * @param status Text content of status. * @param options.media_ids Array of Attachment ids. * @param options.poll Poll object. * @param options.in_reply_to_id ID of the status being replied to, if status is a reply. * @param options.sensitive Mark status and attached media as sensitive? * @param options.spoiler_text Text to be shown as a warning or subject before the actual content. * @param options.visibility Visibility of the posted status. * @param options.scheduled_at ISO 8601 Datetime at which to schedule a status. * @param options.language ISO 639 language code for this status. * @param options.quote_id ID of the status being quoted to, if status is a quote. * @return Status. When options.scheduled_at is present, ScheduledStatus is returned instead. */ public async postStatus( status: string, options: { media_ids?: Array poll?: { options: Array; expires_in: number; multiple?: boolean; hide_totals?: boolean } in_reply_to_id?: string sensitive?: boolean spoiler_text?: string visibility?: 'public' | 'unlisted' | 'private' | 'direct' scheduled_at?: string language?: string quote_id?: string } ): Promise> { let params = { status: status } if (options) { if (options.media_ids) { params = Object.assign(params, { media_ids: options.media_ids }) } if (options.poll) { let pollParam = { options: options.poll.options, expires_in: options.poll.expires_in } if (options.poll.multiple !== undefined) { pollParam = Object.assign(pollParam, { multiple: options.poll.multiple }) } if (options.poll.hide_totals !== undefined) { pollParam = Object.assign(pollParam, { hide_totals: options.poll.hide_totals }) } params = Object.assign(params, { poll: pollParam }) } if (options.in_reply_to_id) { params = Object.assign(params, { in_reply_to_id: options.in_reply_to_id }) } if (options.sensitive !== undefined) { params = Object.assign(params, { sensitive: options.sensitive }) } if (options.spoiler_text) { params = Object.assign(params, { spoiler_text: options.spoiler_text }) } if (options.visibility) { params = Object.assign(params, { visibility: options.visibility }) } if (options.scheduled_at) { params = Object.assign(params, { scheduled_at: options.scheduled_at }) } if (options.language) { params = Object.assign(params, { language: options.language }) } if (options.quote_id) { params = Object.assign(params, { quote_id: options.quote_id }) } } if (options && options.scheduled_at) { return this.client.post('/api/v1/statuses', params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.scheduled_status(res.data) }) }) } return this.client.post('/api/v1/statuses', params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** * GET /api/v1/statuses/:id * * @param id The target status id. * @return Status */ public async getStatus(id: string): Promise> { return this.client.get(`/api/v1/statuses/${id}`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** PUT /api/v1/statuses/:id * * @param id The target status id. * @return Status */ public async editStatus( id: string, options: { status?: string spoiler_text?: string sensitive?: boolean media_ids?: Array poll?: { options?: Array; expires_in?: number; multiple?: boolean; hide_totals?: boolean } } ): Promise> { let params = {} if (options.status) { params = Object.assign(params, { status: options.status }) } if (options.spoiler_text) { params = Object.assign(params, { spoiler_text: options.spoiler_text }) } if (options.sensitive) { params = Object.assign(params, { sensitive: options.sensitive }) } if (options.media_ids) { params = Object.assign(params, { media_ids: options.media_ids }) } if (options.poll) { let pollParam = {} if (options.poll.options !== undefined) { pollParam = Object.assign(pollParam, { options: options.poll.options }) } if (options.poll.expires_in !== undefined) { pollParam = Object.assign(pollParam, { expires_in: options.poll.expires_in }) } if (options.poll.multiple !== undefined) { pollParam = Object.assign(pollParam, { multiple: options.poll.multiple }) } if (options.poll.hide_totals !== undefined) { pollParam = Object.assign(pollParam, { hide_totals: options.poll.hide_totals }) } params = Object.assign(params, { poll: pollParam }) } return this.client.put(`/api/v1/statuses/${id}`, params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** * DELETE /api/v1/statuses/:id * * @param id The target status id. * @return Status */ public async deleteStatus(id: string): Promise> { return this.client.del(`/api/v1/statuses/${id}`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** * GET /api/v1/statuses/:id/context * * Get parent and child statuses. * @param id The target status id. * @return Context */ public async getStatusContext( id: string, options?: { limit?: number; max_id?: string; since_id?: string } ): Promise> { let params = {} if (options) { if (options.limit) { params = Object.assign(params, { limit: options.limit }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } } return this.client.get(`/api/v1/statuses/${id}/context`, params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.context(res.data) }) }) } /** * GET /api/v1/statuses/:id/source * * Obtain the source properties for a status so that it can be edited. * @param id The target status id. * @return StatusSource */ public async getStatusSource(id: string): Promise> { return this.client.get(`/api/v1/statuses/${id}/source`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status_source(res.data) }) }) } /** * GET /api/v1/statuses/:id/reblogged_by * * @param id The target status id. * @return Array of accounts. */ public async getStatusRebloggedBy(id: string): Promise>> { return this.client.get>(`/api/v1/statuses/${id}/reblogged_by`).then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) }) } /** * GET /api/v1/statuses/:id/favourited_by * * @param id The target status id. * @return Array of accounts. */ public async getStatusFavouritedBy(id: string): Promise>> { return this.client.get>(`/api/v1/statuses/${id}/favourited_by`).then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) }) } /** * POST /api/v1/statuses/:id/favourite * * @param id The target status id. * @return Status. */ public async favouriteStatus(id: string): Promise> { return this.client.post(`/api/v1/statuses/${id}/favourite`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** * POST /api/v1/statuses/:id/unfavourite * * @param id The target status id. * @return Status. */ public async unfavouriteStatus(id: string): Promise> { return this.client.post(`/api/v1/statuses/${id}/unfavourite`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** * POST /api/v1/statuses/:id/reblog * * @param id The target status id. * @return Status. */ public async reblogStatus(id: string): Promise> { return this.client.post(`/api/v1/statuses/${id}/reblog`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** * POST /api/v1/statuses/:id/unreblog * * @param id The target status id. * @return Status. */ public async unreblogStatus(id: string): Promise> { return this.client.post(`/api/v1/statuses/${id}/unreblog`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** * POST /api/v1/statuses/:id/bookmark * * @param id The target status id. * @return Status. */ public async bookmarkStatus(id: string): Promise> { return this.client.post(`/api/v1/statuses/${id}/bookmark`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** * POST /api/v1/statuses/:id/unbookmark * * @param id The target status id. * @return Status. */ public async unbookmarkStatus(id: string): Promise> { return this.client.post(`/api/v1/statuses/${id}/unbookmark`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** * POST /api/v1/statuses/:id/mute * * @param id The target status id. * @return Status */ public async muteStatus(id: string): Promise> { return this.client.post(`/api/v1/statuses/${id}/mute`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** * POST /api/v1/statuses/:id/unmute * * @param id The target status id. * @return Status */ public async unmuteStatus(id: string): Promise> { return this.client.post(`/api/v1/statuses/${id}/unmute`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** * POST /api/v1/statuses/:id/pin * @param id The target status id. * @return Status */ public async pinStatus(id: string): Promise> { return this.client.post(`/api/v1/statuses/${id}/pin`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } /** * POST /api/v1/statuses/:id/unpin * * @param id The target status id. * @return Status */ public async unpinStatus(id: string): Promise> { return this.client.post(`/api/v1/statuses/${id}/unpin`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.status(res.data) }) }) } // ====================================== // statuses/media // ====================================== /** * POST /api/v2/media * * @param file The file to be attached, using multipart form data. * @param options.description A plain-text description of the media. * @param options.focus Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. * @return Attachment */ public async uploadMedia( file: any, options?: { description?: string; focus?: string } ): Promise> { const formData = new FormData() formData.append('file', file) if (options) { if (options.description) { formData.append('description', options.description) } if (options.focus) { formData.append('focus', options.focus) } } return this.client.postForm('/api/v2/media', formData).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.async_attachment(res.data) }) }) } /** * GET /api/v1/media/:id * * @param id Target media ID. * @return Attachment */ public async getMedia(id: string): Promise> { const res = await this.client.get(`/api/v1/media/${id}`) return Object.assign(res, { data: MastodonAPI.Converter.attachment(res.data) }) } /** * PUT /api/v1/media/:id * * @param id Target media ID. * @param options.file The file to be attached, using multipart form data. * @param options.description A plain-text description of the media. * @param options.focus Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. * @param options.is_sensitive Whether the media is sensitive. * @return Attachment */ public async updateMedia( id: string, options?: { file?: any description?: string focus?: string } ): Promise> { const formData = new FormData() if (options) { if (options.file) { formData.append('file', options.file) } if (options.description) { formData.append('description', options.description) } if (options.focus) { formData.append('focus', options.focus) } } return this.client.putForm(`/api/v1/media/${id}`, formData).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.attachment(res.data) }) }) } // ====================================== // statuses/polls // ====================================== /** * GET /api/v1/polls/:id * * @param id Target poll ID. * @return Poll */ public async getPoll(id: string): Promise> { return this.client.get(`/api/v1/polls/${id}`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.poll(res.data) }) }) } /** * POST /api/v1/polls/:id/votes * * @param id Target poll ID. * @param choices Array of own votes containing index for each option (starting from 0). * @return Poll */ public async votePoll(id: string, choices: Array): Promise> { return this.client .post(`/api/v1/polls/${id}/votes`, { choices: choices }) .then(res => { return Object.assign(res, { data: MastodonAPI.Converter.poll(res.data) }) }) } // ====================================== // statuses/scheduled_statuses // ====================================== /** * GET /api/v1/scheduled_statuses * * @param options.limit Max number of results to return. Defaults to 20. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of scheduled statuses. */ public async getScheduledStatuses(options?: { limit?: number | null max_id?: string | null since_id?: string | null min_id?: string | null }): Promise>> { let params = {} if (options) { if (options.limit) { params = Object.assign(params, { limit: options.limit }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } } return this.client.get>('/api/v1/scheduled_statuses', params).then(res => { return Object.assign(res, { data: res.data.map(s => MastodonAPI.Converter.scheduled_status(s)) }) }) } /** * GET /api/v1/scheduled_statuses/:id * * @param id Target status ID. * @return ScheduledStatus. */ public async getScheduledStatus(id: string): Promise> { return this.client.get(`/api/v1/scheduled_statuses/${id}`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.scheduled_status(res.data) }) }) } /** * PUT /api/v1/scheduled_statuses/:id * * @param id Target scheduled status ID. * @param scheduled_at ISO 8601 Datetime at which the status will be published. * @return ScheduledStatus. */ public async scheduleStatus(id: string, scheduled_at?: string | null): Promise> { let params = {} if (scheduled_at) { params = Object.assign(params, { scheduled_at: scheduled_at }) } return this.client.put(`/api/v1/scheduled_statuses/${id}`, params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.scheduled_status(res.data) }) }) } /** * DELETE /api/v1/scheduled_statuses/:id * * @param id Target scheduled status ID. */ public cancelScheduledStatus(id: string): Promise> { return this.client.del<{}>(`/api/v1/scheduled_statuses/${id}`) } // ====================================== // timelines // ====================================== /** * GET /api/v1/timelines/public * * @param options.only_media Show only statuses with media attached? Defaults to false. * @param options.limit Max number of results to return. Defaults to 20. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of statuses. */ public async getPublicTimeline(options?: { only_media?: boolean limit?: number max_id?: string since_id?: string min_id?: string }): Promise>> { let params = { local: false } if (options) { if (options.only_media !== undefined) { params = Object.assign(params, { only_media: options.only_media }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.client.get>('/api/v1/timelines/public', params).then(res => { return Object.assign(res, { data: res.data.map(s => MastodonAPI.Converter.status(s)) }) }) } /** * GET /api/v1/timelines/public * * @param options.only_media Show only statuses with media attached? Defaults to false. * @param options.limit Max number of results to return. Defaults to 20. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of statuses. */ public async getLocalTimeline(options?: { only_media?: boolean limit?: number max_id?: string since_id?: string min_id?: string }): Promise>> { let params = { local: true } if (options) { if (options.only_media !== undefined) { params = Object.assign(params, { only_media: options.only_media }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.client.get>('/api/v1/timelines/public', params).then(res => { return Object.assign(res, { data: res.data.map(s => MastodonAPI.Converter.status(s)) }) }) } /** * GET /api/v1/timelines/tag/:hashtag * * @param hashtag Content of a #hashtag, not including # symbol. * @param options.local Show only local statuses? Defaults to false. * @param options.only_media Show only statuses with media attached? Defaults to false. * @param options.limit Max number of results to return. Defaults to 20. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of statuses. */ public async getTagTimeline( hashtag: string, options?: { local?: boolean only_media?: boolean limit?: number max_id?: string since_id?: string min_id?: string } ): Promise>> { let params = {} if (options) { if (options.local !== undefined) { params = Object.assign(params, { local: options.local }) } if (options.only_media !== undefined) { params = Object.assign(params, { only_media: options.only_media }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.client.get>(`/api/v1/timelines/tag/${hashtag}`, params).then(res => { return Object.assign(res, { data: res.data.map(s => MastodonAPI.Converter.status(s)) }) }) } /** * GET /api/v1/timelines/home * * @param options.local Show only local statuses? Defaults to false. * @param options.limit Max number of results to return. Defaults to 20. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of statuses. */ public async getHomeTimeline(options?: { local?: boolean limit?: number max_id?: string since_id?: string min_id?: string }): Promise>> { let params = {} if (options) { if (options.local !== undefined) { params = Object.assign(params, { local: options.local }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.client.get>('/api/v1/timelines/home', params).then(res => { return Object.assign(res, { data: res.data.map(s => MastodonAPI.Converter.status(s)) }) }) } /** * GET /api/v1/timelines/list/:list_id * * @param list_id Local ID of the list in the database. * @param options.limit Max number of results to return. Defaults to 20. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of statuses. */ public async getListTimeline( list_id: string, options?: { limit?: number max_id?: string since_id?: string min_id?: string } ): Promise>> { let params = {} if (options) { if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.client.get>(`/api/v1/timelines/list/${list_id}`, params).then(res => { return Object.assign(res, { data: res.data.map(s => MastodonAPI.Converter.status(s)) }) }) } // ====================================== // timelines/conversations // ====================================== /** * GET /api/v1/conversations * * @param options.limit Max number of results to return. Defaults to 20. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of statuses. */ public async getConversationTimeline(options?: { limit?: number max_id?: string since_id?: string min_id?: string }): Promise>> { let params = {} if (options) { if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } } return this.client.get>('/api/v1/conversations', params).then(res => { return Object.assign(res, { data: res.data.map(c => MastodonAPI.Converter.conversation(c)) }) }) } /** * DELETE /api/v1/conversations/:id * * @param id Target conversation ID. */ public deleteConversation(id: string): Promise> { return this.client.del<{}>(`/api/v1/conversations/${id}`) } /** * POST /api/v1/conversations/:id/read * * @param id Target conversation ID. * @return Conversation. */ public async readConversation(id: string): Promise> { return this.client.post(`/api/v1/conversations/${id}/read`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.conversation(res.data) }) }) } // ====================================== // timelines/lists // ====================================== /** * GET /api/v1/lists * * @return Array of lists. */ public async getLists(): Promise>> { return this.client.get>('/api/v1/lists').then(res => { return Object.assign(res, { data: res.data.map(l => MastodonAPI.Converter.list(l)) }) }) } /** * GET /api/v1/lists/:id * * @param id Target list ID. * @return List. */ public async getList(id: string): Promise> { return this.client.get(`/api/v1/lists/${id}`).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.list(res.data) }) }) } /** * POST /api/v1/lists * * @param title List name. * @return List. */ public async createList(title: string): Promise> { return this.client .post('/api/v1/lists', { title: title }) .then(res => { return Object.assign(res, { data: MastodonAPI.Converter.list(res.data) }) }) } /** * PUT /api/v1/lists/:id * * @param id Target list ID. * @param title New list name. * @return List. */ public async updateList(id: string, title: string): Promise> { return this.client .put(`/api/v1/lists/${id}`, { title: title }) .then(res => { return Object.assign(res, { data: MastodonAPI.Converter.list(res.data) }) }) } /** * DELETE /api/v1/lists/:id * * @param id Target list ID. */ public deleteList(id: string): Promise> { return this.client.del<{}>(`/api/v1/lists/${id}`) } /** * GET /api/v1/lists/:id/accounts * * @param id Target list ID. * @param options.limit Max number of results to return. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @param options.min_id Return results immediately newer than ID. * @return Array of accounts. */ public async getAccountsInList( id: string, options?: { limit?: number max_id?: string since_id?: string } ): Promise>> { let params = {} if (options) { if (options.limit) { params = Object.assign(params, { limit: options.limit }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } } return this.client.get>(`/api/v1/lists/${id}/accounts`, params).then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) }) } /** * POST /api/v1/lists/:id/accounts * * @param id Target list ID. * @param account_ids Array of account IDs to add to the list. */ public addAccountsToList(id: string, account_ids: Array): Promise> { return this.client.post<{}>(`/api/v1/lists/${id}/accounts`, { account_ids: account_ids }) } /** * DELETE /api/v1/lists/:id/accounts * * @param id Target list ID. * @param account_ids Array of account IDs to add to the list. */ public deleteAccountsFromList(id: string, account_ids: Array): Promise> { return this.client.del<{}>(`/api/v1/lists/${id}/accounts`, { account_ids: account_ids }) } // ====================================== // timelines/markers // ====================================== /** * GET /api/v1/markers * * @param timelines Array of timeline names, String enum anyOf home, notifications. * @return Marker or empty object. */ public async getMarkers(timeline: Array): Promise>> { return this.client .get>('/api/v1/markers', { timeline: timeline }) .then(res => { return Object.assign(res, { data: MastodonAPI.Converter.marker(res.data) }) }) } /** * POST /api/v1/markers * * @param options.home Marker position of the last read status ID in home timeline. * @param options.notifications Marker position of the last read notification ID in notifications. * @return Marker. */ public async saveMarkers(options?: { home?: { last_read_id: string } notifications?: { last_read_id: string } }): Promise> { let params = {} if (options) { if (options.home) { params = Object.assign(params, { home: options.home }) } if (options.notifications) { params = Object.assign(params, { notifications: options.notifications }) } } return this.client.post('/api/v1/markers', params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.marker(res.data) }) }) } // ====================================== // notifications // ====================================== /** * GET /api/v1/notifications * * @param options.limit Max number of results to return. Defaults to 20. * @param options.max_id Return results older than ID. * @param options.since_id Return results newer than ID. * @param options.min_id Return results immediately newer than ID. * @param options.exclude_types Array of types to exclude. * @param options.account_id Return only notifications received from this account. * @return Array of notifications. */ public async getNotifications(options?: { limit?: number max_id?: string since_id?: string min_id?: string exclude_types?: Array account_id?: string }): Promise>> { let params = {} if (options) { if (options.limit) { params = Object.assign(params, { limit: options.limit }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.since_id) { params = Object.assign(params, { since_id: options.since_id }) } if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.exclude_types) { params = Object.assign(params, { exclude_types: options.exclude_types.map(e => MastodonAPI.Converter.encodeNotificationType(e)) }) } if (options.account_id) { params = Object.assign(params, { account_id: options.account_id }) } } return this.client.get>('/api/v1/notifications', params).then(res => { return Object.assign(res, { data: res.data.flatMap(n => { const notify = MastodonAPI.Converter.notification(n) if (notify instanceof UnknownNotificationTypeError) return [] return notify }) }) }) } /** * GET /api/v1/notifications/:id * * @param id Target notification ID. * @return Notification. */ public async getNotification(id: string): Promise> { const res = await this.client.get(`/api/v1/notifications/${id}`) const notify = MastodonAPI.Converter.notification(res.data) if (notify instanceof UnknownNotificationTypeError) { throw new UnknownNotificationTypeError() } return { ...res, data: notify } } /** * POST /api/v1/notifications/clear */ public dismissNotifications(): Promise> { return this.client.post<{}>('/api/v1/notifications/clear') } /** * POST /api/v1/notifications/:id/dismiss * * @param id Target notification ID. */ public dismissNotification(id: string): Promise> { return this.client.post<{}>(`/api/v1/notifications/${id}/dismiss`) } public readNotifications(_options: { id?: string max_id?: string }): Promise>> { return new Promise((_, reject) => { const err = new NoImplementedError('mastodon does not support') reject(err) }) } // ====================================== // notifications/push // ====================================== /** * POST /api/v1/push/subscription * * @param subscription[endpoint] Endpoint URL that is called when a notification event occurs. * @param subscription[keys][p256dh] User agent public key. Base64 encoded string of public key of ECDH key using prime256v1 curve. * @param subscription[keys] Auth secret. Base64 encoded string of 16 bytes of random data. * @param data[alerts][follow] Receive follow notifications? * @param data[alerts][favourite] Receive favourite notifications? * @param data[alerts][reblog] Receive reblog notifictaions? * @param data[alerts][mention] Receive mention notifications? * @param data[alerts][poll] Receive poll notifications? * @return PushSubscription. */ public async subscribePushNotification( subscription: { endpoint: string; keys: { p256dh: string; auth: string } }, data?: { alerts: { follow?: boolean; favourite?: boolean; reblog?: boolean; mention?: boolean; poll?: boolean } } | null ): Promise> { let params = { subscription } if (data) { params = Object.assign(params, { data }) } return this.client.post('/api/v1/push/subscription', params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.push_subscription(res.data) }) }) } /** * GET /api/v1/push/subscription * * @return PushSubscription. */ public async getPushSubscription(): Promise> { return this.client.get('/api/v1/push/subscription').then(res => { return Object.assign(res, { data: MastodonAPI.Converter.push_subscription(res.data) }) }) } /** * PUT /api/v1/push/subscription * * @param data[alerts][follow] Receive follow notifications? * @param data[alerts][favourite] Receive favourite notifications? * @param data[alerts][reblog] Receive reblog notifictaions? * @param data[alerts][mention] Receive mention notifications? * @param data[alerts][poll] Receive poll notifications? * @return PushSubscription. */ public async updatePushSubscription( data?: { alerts: { follow?: boolean; favourite?: boolean; reblog?: boolean; mention?: boolean; poll?: boolean } } | null ): Promise> { let params = {} if (data) { params = Object.assign(params, { data }) } return this.client.put('/api/v1/push/subscription', params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.push_subscription(res.data) }) }) } /** * DELETE /api/v1/push/subscription */ public deletePushSubscription(): Promise> { return this.client.del<{}>('/api/v1/push/subscription') } // ====================================== // search // ====================================== /** * GET /api/v2/search * * @param q The search query. * @param type Enum of search target. * @param options.limit Maximum number of results to load, per type. Defaults to 20. Max 40. * @param options.max_id Return results older than this id. * @param options.min_id Return results immediately newer than this id. * @param options.resolve Attempt WebFinger lookup. Defaults to false. * @param options.following Only include accounts that the user is following. Defaults to false. * @param options.account_id If provided, statuses returned will be authored only by this account. * @param options.exclude_unreviewed Filter out unreviewed tags? Defaults to false. * @return Results. */ public async search( q: string, options?: { type?: 'accounts' | 'hashtags' | 'statuses' limit?: number max_id?: string min_id?: string resolve?: boolean offset?: number following?: boolean account_id?: string exclude_unreviewed?: boolean } ): Promise> { let params = { q } if (options) { if (options.type) { params = Object.assign(params, { type: options.type }) } if (options.limit) { params = Object.assign(params, { limit: options.limit }) } if (options.max_id) { params = Object.assign(params, { max_id: options.max_id }) } if (options.min_id) { params = Object.assign(params, { min_id: options.min_id }) } if (options.resolve !== undefined) { params = Object.assign(params, { resolve: options.resolve }) } if (options.offset) { params = Object.assign(params, { offset: options.offset }) } if (options.following !== undefined) { params = Object.assign(params, { following: options.following }) } if (options.account_id) { params = Object.assign(params, { account_id: options.account_id }) } if (options.exclude_unreviewed) { params = Object.assign(params, { exclude_unreviewed: options.exclude_unreviewed }) } } return this.client.get('/api/v2/search', params).then(res => { return Object.assign(res, { data: MastodonAPI.Converter.results(res.data) }) }) } // ====================================== // instance // ====================================== /** * GET /api/v1/instance */ public async getInstance(): Promise> { return this.client.get('/api/v1/instance').then(res => { return Object.assign(res, { data: MastodonAPI.Converter.instance(res.data) }) }) } /** * GET /api/v1/instance/peers */ public getInstancePeers(): Promise>> { return this.client.get>('/api/v1/instance/peers') } /** * GET /api/v1/instance/activity */ public async getInstanceActivity(): Promise>> { return this.client.get>('/api/v1/instance/activity').then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.activity(a)) }) }) } // ====================================== // instance/trends // ====================================== /** * GET /api/v1/trends * * @param limit Maximum number of results to return. Defaults to 10. */ public async getInstanceTrends(limit?: number | null): Promise>> { let params = {} if (limit) { params = Object.assign(params, { limit }) } return this.client.get>('/api/v1/trends', params).then(res => { return Object.assign(res, { data: res.data.map(t => MastodonAPI.Converter.tag(t)) }) }) } // ====================================== // instance/directory // ====================================== /** * GET /api/v1/directory * * @param options.limit How many accounts to load. Default 40. * @param options.offset How many accounts to skip before returning results. Default 0. * @param options.order Order of results. * @param options.local Only return local accounts. * @return Array of accounts. */ public async getInstanceDirectory(options?: { limit?: number offset?: number order?: 'active' | 'new' local?: boolean }): Promise>> { let params = {} if (options) { if (options.limit) { params = Object.assign(params, { limit: options.limit }) } if (options.offset) { params = Object.assign(params, { offset: options.offset }) } if (options.order) { params = Object.assign(params, { order: options.order }) } if (options.local !== undefined) { params = Object.assign(params, { local: options.local }) } } return this.client.get>('/api/v1/directory', params).then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.account(a)) }) }) } // ====================================== // instance/custom_emojis // ====================================== /** * GET /api/v1/custom_emojis * * @return Array of emojis. */ public async getInstanceCustomEmojis(): Promise>> { return this.client.get>('/api/v1/custom_emojis').then(res => { return Object.assign(res, { data: res.data.map(e => MastodonAPI.Converter.emoji(e)) }) }) } // ====================================== // instance/announcements // ====================================== /** * GET /api/v1/announcements * * @return Array of announcements. */ public async getInstanceAnnouncements(): Promise>> { return this.client.get>('/api/v1/announcements').then(res => { return Object.assign(res, { data: res.data.map(a => MastodonAPI.Converter.announcement(a)) }) }) } /** * POST /api/v1/announcements/:id/dismiss * * @param id The ID of the Announcement in the database. */ public async dismissInstanceAnnouncement(id: string): Promise>> { return this.client.post>(`/api/v1/announcements/${id}/dismiss`) } /** * PUT /api/v1/announcements/:id/reactions/:name * * @param id The ID of the Announcement in the database. * @param name Unicode emoji, or the shortcode of a custom emoji. */ public async addReactionToAnnouncement(id: string, name: string): Promise>> { return this.client.put>(`/api/v1/announcements/${id}/reactions/${name}`) } /** * DELETE /api/v1/announcements/:id/reactions/:name * * @param id The ID of the Announcement in the database. * @param name Unicode emoji, or the shortcode of a custom emoji. */ public async removeReactionFromAnnouncement(id: string, name: string): Promise>> { return this.client.del>(`/api/v1/announcements/${id}/reactions/${name}`) } // ====================================== // Emoji reactions // ====================================== public async createEmojiReaction(_id: string, _emoji: string): Promise> { return new Promise((_, reject) => { const err = new NoImplementedError('mastodon does not support') reject(err) }) } public async deleteEmojiReaction(_id: string, _emoji: string): Promise> { return new Promise((_, reject) => { const err = new NoImplementedError('mastodon does not support') reject(err) }) } public async getEmojiReactions(_id: string): Promise>> { return new Promise((_, reject) => { const err = new NoImplementedError('mastodon does not support') reject(err) }) } public async getEmojiReaction(_id: string, _emoji: string): Promise> { return new Promise((_, reject) => { const err = new NoImplementedError('mastodon does not support') reject(err) }) } // ====================================== // WebSocket // ====================================== public userSocket(): WebSocket { return this.client.socket('/api/v1/streaming', 'user') } public publicSocket(): WebSocket { return this.client.socket('/api/v1/streaming', 'public') } public localSocket(): WebSocket { return this.client.socket('/api/v1/streaming', 'public:local') } public tagSocket(tag: string): WebSocket { return this.client.socket('/api/v1/streaming', 'hashtag', `tag=${tag}`) } public listSocket(list_id: string): WebSocket { return this.client.socket('/api/v1/streaming', 'list', `list=${list_id}`) } public directSocket(): WebSocket { return this.client.socket('/api/v1/streaming', 'direct') } }