UNPKG

@cutls/megalodon

Version:

Mastodon, Pleroma, Misskey API client for node.js and browser

587 lines (586 loc) 15.7 kB
import axios from 'axios' import dayjs from 'dayjs' import FormData from 'form-data' import { DEFAULT_UA } from '../default.js' import WebSocket from './web_socket.js' import FirefishNotificationType from './notification.js' import NotificationType, { UnknownNotificationTypeError } from '../notification.js' var FirefishAPI ;((FirefishAPI) => { let Converter ;((Converter) => { Converter.announcement = (a) => ({ id: a.id, content: a.title + '\n' + a.text, starts_at: null, ends_at: null, published: true, all_day: true, published_at: a.createdAt, updated_at: a.updatedAt, read: a.isRead !== undefined ? a.isRead : null, mentions: [], statuses: [], tags: [], emojis: [], reactions: [] }) Converter.emoji = (e) => { return { shortcode: e.name, static_url: e.url, url: e.url, visible_in_picker: true, category: e.category ? e.category : undefined } } Converter.user = (u) => { let acct = u.username if (u.host) { acct = `${u.username}@${u.host}` } return { id: u.id, username: u.username, acct: acct, display_name: u.name ? u.name : '', locked: false, group: null, noindex: u.isIndexable !== undefined ? u.isIndexable : null, suspended: null, limited: null, created_at: new Date().toISOString(), followers_count: 0, following_count: 0, statuses_count: 0, note: '', url: acct, avatar: u.avatarUrl ?? '', avatar_static: u.avatarColor ?? '', header: '', header_static: '', emojis: Array.isArray(u.emojis) ? u.emojis.map((e) => Converter.emoji(e)) : [], moved: null, fields: [], bot: null } } Converter.userDetail = (u) => { let acct = u.username if (u.host) { acct = `${u.username}@${u.host}` } return { id: u.id, username: u.username, acct: acct, display_name: u.name ?? '', locked: u.isLocked, group: null, noindex: u.isIndexable !== undefined ? u.isIndexable : null, suspended: u.isSuspended, limited: u.isSilenced, created_at: u.createdAt, followers_count: u.followersCount, following_count: u.followingCount, statuses_count: u.notesCount, note: u.description ?? '', url: acct, avatar: u.avatarUrl ?? '', avatar_static: u.avatarColor ?? '', header: u.bannerUrl ?? '', header_static: u.bannerColor ?? '', emojis: Array.isArray(u.emojis) ? u.emojis.map((e) => Converter.emoji(e)) : [], moved: null, fields: u.fields.map((f) => field(f)), bot: u.isBot !== undefined ? u.isBot : null, source: { privacy: null, sensitive: null, language: u.lang, note: u.description ?? '', fields: [] } } } Converter.userDetailMe = (u) => { const account = Converter.userDetail(u) return Object.assign({}, account, { source: { privacy: null, sensitive: u.alwaysMarkNsfw, language: u.lang, note: u.description ?? '', fields: [] } }) } Converter.userPreferences = (u, v) => { return { 'reading:expand:media': 'default', 'reading:expand:spoilers': false, 'posting:default:language': u.lang, 'posting:default:sensitive': u.alwaysMarkNsfw, 'posting:default:visibility': v } } Converter.visibility = (v) => { switch (v) { case 'public': return v case 'home': return 'unlisted' case 'followers': return 'private' case 'specified': return 'direct' case 'hidden': return 'local' default: return 'public' } } Converter.encodeVisibility = (v) => { switch (v) { case 'public': return v case 'unlisted': return 'home' case 'private': return 'followers' case 'direct': return 'specified' case 'local': return 'hidden' } } Converter.fileType = (s) => { if (s === 'image/gif') { return 'gifv' } if (s.includes('image')) { return 'image' } if (s.includes('video')) { return 'video' } if (s.includes('audio')) { return 'audio' } return 'unknown' } Converter.file = (f) => { return { id: f.id, type: Converter.fileType(f.type), url: f.url ? f.url : '', remote_url: f.url, preview_url: f.thumbnailUrl, text_url: f.url, meta: { width: f.properties.width, height: f.properties.height }, description: f.comment, blurhash: f.blurhash } } Converter.follower = (f) => { return Converter.user(f.follower) } Converter.following = (f) => { return Converter.user(f.followee) } Converter.relation = (r) => { return { id: r.id, following: r.isFollowing, followed_by: r.isFollowed, blocking: r.isBlocking, blocked_by: r.isBlocked, muting: r.isMuted, muting_notifications: false, requested: r.hasPendingFollowRequestFromYou, domain_blocking: false, showing_reblogs: true, endorsed: false, notifying: false, note: null } } Converter.choice = (c) => { return { title: c.text, votes_count: c.votes } } Converter.poll = (p) => { const now = dayjs() const expire = dayjs(p.expiresAt) const count = p.choices.reduce((sum, choice) => sum + choice.votes, 0) return { id: '', expires_at: p.expiresAt, expired: now.isAfter(expire), multiple: p.multiple, votes_count: count, options: Array.isArray(p.choices) ? p.choices.map((c) => Converter.choice(c)) : [], voted: Array.isArray(p.choices) ? p.choices.some((c) => c.isVoted) : false } } Converter.note = (n) => { return { id: n.id, uri: n.uri ? n.uri : '', url: n.uri ? n.uri : '', account: Converter.user(n.user), in_reply_to_id: n.replyId ? n.replyId : null, in_reply_to_account_id: n.reply?.userId ?? null, reblog: n.renote ? Converter.note(n.renote) : null, content: n.text ? n.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/`/g, '&#x60;').replace(/\r?\n/g, '<br>') : '', plain_content: n.text ? n.text : null, created_at: n.createdAt, edited_at: null, emojis: Array.isArray(n.emojis) ? n.emojis.filter((e) => !e.name.includes('@')).map((e) => Converter.emoji(e)) : [], replies_count: n.repliesCount, reblogs_count: n.renoteCount, favourites_count: 0, reblogged: false, favourited: !!n.myReaction, muted: false, sensitive: Array.isArray(n.files) ? n.files.some((f) => f.isSensitive) : false, spoiler_text: n.cw ? n.cw : '', visibility: Converter.visibility(n.visibility), media_attachments: Array.isArray(n.files) ? n.files.map((f) => Converter.file(f)) : [], mentions: [], tags: [], card: null, poll: n.poll ? Converter.poll(n.poll) : null, application: null, language: null, pinned: null, emoji_reactions: Converter.mapReactions(n.emojis ? n.emojis : [], n.reactions, n.myReaction), bookmarked: false, quote: n.renote !== undefined && n.text !== null } } Converter.mapReactions = (emojis, r, myReaction) => { const emojiUrls = new Map(emojis.map((e) => [e.name, e.url])) return Object.keys(r).map((key) => { const shortcode = key.replace(/:/g, '') const url = emojiUrls.get(shortcode) const name = shortcode.replace('@.', '') return { count: r[key], me: key === myReaction, name, url, static_url: url } }) } Converter.reactions = (r) => { const result = [] r.map((e) => { const i = result.findIndex((res) => res.name === e.type) if (i >= 0) { result[i].count++ } else { result.push({ count: 1, me: false, name: e.type }) } }) return result } Converter.noteToConversation = (n) => { const accounts = [Converter.user(n.user)] if (n.reply) { accounts.push(Converter.user(n.reply.user)) } return { id: n.id, accounts: accounts, last_status: Converter.note(n), unread: false } } Converter.list = (l) => ({ id: l.id, title: l.name, replies_policy: null }) Converter.encodeNotificationType = (e) => { switch (e) { case NotificationType.Follow: return FirefishNotificationType.Follow case NotificationType.Mention: return FirefishNotificationType.Reply case NotificationType.Favourite: case NotificationType.Reaction: return FirefishNotificationType.Reaction case NotificationType.Reblog: return FirefishNotificationType.Renote case NotificationType.PollVote: return FirefishNotificationType.PollVote case NotificationType.FollowRequest: return FirefishNotificationType.ReceiveFollowRequest default: return new UnknownNotificationTypeError() } } Converter.decodeNotificationType = (e) => { switch (e) { case FirefishNotificationType.Follow: return NotificationType.Follow case FirefishNotificationType.Mention: case FirefishNotificationType.Reply: return NotificationType.Mention case FirefishNotificationType.Renote: case FirefishNotificationType.Quote: return NotificationType.Reblog case FirefishNotificationType.Reaction: return NotificationType.Reaction case FirefishNotificationType.PollVote: return NotificationType.PollVote case FirefishNotificationType.ReceiveFollowRequest: return NotificationType.FollowRequest case FirefishNotificationType.FollowRequestAccepted: return NotificationType.Follow default: return new UnknownNotificationTypeError() } } Converter.notification = (n) => { const notificationType = Converter.decodeNotificationType(n.type) if (notificationType instanceof UnknownNotificationTypeError) { return notificationType } let notification = { id: n.id, account: n.user ? Converter.user(n.user) : null, created_at: n.createdAt, type: notificationType } if (n.note) { notification = Object.assign(notification, { status: Converter.note(n.note) }) } if (n.reaction) { const reactions = Converter.mapReactions(n.note?.emojis ?? [], { [n.reaction]: 1 }) if (reactions.length > 0) { notification = Object.assign(notification, { reaction: reactions[0] }) } } return notification } Converter.stats = (s) => { return { user_count: s.usersCount, status_count: s.notesCount, domain_count: s.instances } } Converter.meta = (m, s) => { const wss = m.uri.replace(/^https:\/\//, 'wss://') return { uri: m.uri, title: m.name, description: m.description ? m.description : '', email: m.maintainerEmail ? m.maintainerEmail : '', version: m.version, thumbnail: m.bannerUrl, urls: { streaming_api: `${wss}/streaming` }, stats: Converter.stats(s), languages: m.langs, registrations: !m.disableRegistration, approval_required: false, configuration: { statuses: { max_characters: m.maxNoteTextLength } } } } const account_emoji = (e) => { return { shortcode: e.shortcode, static_url: e.static_url, url: e.url, visible_in_picker: e.visible_in_picker } } const field = (f) => { return { name: f.name, value: f.value, verified: f.verified, verified_at: null } } Converter.instance = (i) => { return { uri: i.uri, title: i.title, description: i.description, email: i.email, version: i.version, thumbnail: i.thumbnail, urls: i.urls, stats: { user_count: i.stats.user_count, status_count: i.stats.status_count, domain_count: i.stats.domain_count }, languages: i.languages, registrations: i.registrations, approval_required: i.approval_required, invites_enabled: i.invites_enabled, configuration: { statuses: { max_characters: i.configuration.statuses.max_characters, max_media_attachments: i.configuration.statuses.max_media_attachments, characters_reserved_per_url: i.configuration.statuses.characters_reserved_per_url }, polls: { max_options: i.configuration.polls.max_options, max_characters_per_option: i.configuration.polls.max_characters_per_option, min_expiration: i.configuration.polls.min_expiration, max_expiration: i.configuration.polls.max_expiration } }, contact_account: { id: i.contact_account.id, username: i.contact_account.username, acct: i.contact_account.acct, display_name: i.contact_account.display_name, locked: i.contact_account.locked, group: null, noindex: null, suspended: null, limited: null, created_at: i.contact_account.created_at, followers_count: i.contact_account.followers_count, following_count: i.contact_account.following_count, statuses_count: i.contact_account.statuses_count, note: i.contact_account.note, url: i.contact_account.url, avatar: i.contact_account.avatar, avatar_static: i.contact_account.avatar_static, header: i.contact_account.header, header_static: i.contact_account.header_static, emojis: i.contact_account.emojis.map((e) => account_emoji(e)), moved: null, fields: i.contact_account.fields.map((f) => field(f)), bot: i.contact_account.bot } } } Converter.hashtag = (h) => { return { name: h.tag, url: h.tag, history: [], following: false } } })((Converter = FirefishAPI.Converter || (FirefishAPI.Converter = {}))) FirefishAPI.DEFAULT_SCOPE = [ 'read:account', 'write:account', 'read:blocks', 'write:blocks', 'read:drive', 'write:drive', 'read:favorites', 'write:favorites', 'read:following', 'write:following', 'read:mutes', 'write:mutes', 'write:notes', 'read:notifications', 'write:notifications', 'read:reactions', 'write:reactions', 'write:votes' ] class Client { accessToken baseUrl userAgent abortController constructor(baseUrl, accessToken, userAgent = DEFAULT_UA) { this.accessToken = accessToken this.baseUrl = baseUrl this.userAgent = userAgent this.abortController = new AbortController() axios.defaults.signal = this.abortController.signal } async get(path, params = {}, headers = {}) { const options = { params: params, headers: headers, maxContentLength: Infinity, maxBodyLength: Infinity } return axios.get(this.baseUrl + path, options).then((resp) => { const res = { data: resp.data, status: resp.status, statusText: resp.statusText, headers: resp.headers } return res }) } async post(path, params = {}, headers = {}) { const options = { headers: headers, maxContentLength: Infinity, maxBodyLength: Infinity } let bodyParams = params if (this.accessToken) { if (params instanceof FormData) { bodyParams.append('i', this.accessToken) } else { bodyParams = Object.assign(params, { i: this.accessToken }) } } return axios.post(this.baseUrl + path, bodyParams, options).then((resp) => { const res = { data: resp.data, status: resp.status, statusText: resp.statusText, headers: resp.headers } return res }) } cancel() { return this.abortController.abort() } socket(url, channel, listId) { if (!this.accessToken) { throw new Error('accessToken is required') } const streaming = new WebSocket(url, channel, this.accessToken, listId, this.userAgent) streaming.start() return streaming } } FirefishAPI.Client = Client })(FirefishAPI || (FirefishAPI = {})) export default FirefishAPI