UNPKG

@cutls/megalodon

Version:

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

599 lines (598 loc) 22.5 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 MisskeyNotificationType from './notification.js'; import NotificationType from '../notification.js'; import { isBrowser } from '../default.js'; import AutolinkerImported from 'autolinker'; export const isEmojiArr = (item) => Array.isArray(item); function autoLinker(input, host) { const Autolinker = AutolinkerImported; return Autolinker.link(input, { hashtag: 'twitter', mention: 'twitter', email: false, stripPrefix: false, replaceFn: (match) => { switch (match.type) { case 'url': return true; case 'mention': return `<a href="https://${host}/@${encodeURIComponent(match.getMention())}" target="_blank">@${match.getMention()}</a>`; case 'hashtag': return `<a href="https://${host}/tags/${encodeURIComponent(match.getHashtag())}" target="_blank">#${match.getHashtag()}</a>`; } return false; } }); } var MisskeyAPI; (function (MisskeyAPI) { let Converter; (function (Converter) { Converter.emoji = (e) => { return { shortcode: e.name, static_url: e.url, url: e.url, visible_in_picker: true }; }; Converter.emojiConverter = (e) => { if (!e) return []; if (isEmojiArr(e)) return e; const emojiArr = Object.entries(e).map(([key, value]) => { return { name: key, host: null, url: value, aliases: [] }; }); return emojiArr; }; Converter.user = (u, host) => { host = host.replace('https://', ''); let acct = u.username; let acctUrl = `https://${host || u.host || 'example.com'}/@${u.username}`; if (u.host) { acct = `${u.username}@${u.host}`; acctUrl = `https://${u.host}/@${u.username}`; } const localEmojis = Converter.emojiExtractor(u.name || '', host).map(e => Converter.emoji(e)); return { id: u.id, username: u.username, acct: acct, display_name: u.name || u.username, locked: false, created_at: new Date().toISOString(), followers_count: 0, following_count: 0, statuses_count: 0, note: '', url: acctUrl, avatar: u.avatarUrl, avatar_static: u.avatarUrl, header: u.avatarUrl, header_static: u.avatarUrl, emojis: [...Converter.emojiConverter(u.emojis).map(e => Converter.emoji(e)), ...localEmojis], moved: null, fields: [], bot: false, group: false, discoverable: false, noindex: false, suspended: false, limited: false }; }; Converter.userDetail = (u, host) => { let acct = u.username; host = host.replace('https://', ''); let acctUrl = `https://${host || u.host || 'example.com'}/@${u.username}`; if (u.host) { acct = `${u.username}@${u.host}`; acctUrl = `https://${u.host}/@${u.username}`; } const localEmojis = Converter.emojiExtractor(`${u.name}${u.description}`, host).map(e => Converter.emoji(e)); return { id: u.id, username: u.username, acct: acct, display_name: u.name || u.username, locked: !!u.isLocked, created_at: u.createdAt || new Date().toISOString(), followers_count: u.followersCount || 0, following_count: u.followingCount || 0, statuses_count: u.notesCount || 0, note: u.description || '', url: acctUrl, avatar: u.avatarUrl || 'https://http.cat/404', avatar_static: u.avatarUrl || 'https://http.cat/404', header: u.bannerUrl || u.avatarUrl || 'https://http.cat/404', header_static: u.bannerUrl || u.avatarUrl || 'https://http.cat/404', emojis: [...Converter.emojiConverter(u.emojis).map(e => Converter.emoji(e)), ...localEmojis], moved: null, fields: [], bot: u.isBot || false, group: false, discoverable: false, noindex: false, suspended: false, limited: false }; }; Converter.visibility = (v) => { switch (v) { case 'public': return v; case 'home': return 'unlisted'; case 'followers': return 'private'; case 'specified': return 'direct'; } }; Converter.encodeVisibility = (v) => { switch (v) { case 'public': return v; case 'unlisted': return 'home'; case 'private': return 'followers'; case 'direct': return 'specified'; } }; 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, remote_url: f.url, preview_url: f.thumbnailUrl, text_url: f.url, meta: { width: f.properties.width, height: f.properties.height }, description: null, blurhash: null }; }; Converter.follower = (f, host) => { return Converter.user(f.follower, host); }; Converter.following = (f, host) => { return Converter.user(f.followee, host); }; 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, requested_by: r.hasPendingFollowRequestToYou, domain_blocking: false, showing_reblogs: true, endorsed: false, notifying: false, note: '' }; }; 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, voters_count: count, options: p.choices.map(c => Converter.choice(c)), voted: p.choices.some(c => c.isVoted) }; }; Converter.note = (n, host) => { host = host.replace('https://', ''); const text = 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>') : ''; const html = autoLinker(text, host); const localEmojis = Converter.emojiExtractor(text, host).map(e => Converter.emoji(e)); return { id: n.id, uri: n.uri ? n.uri : `https://${host}/notes/${n.id}`, url: n.uri ? n.uri : `https://${host}/notes/${n.id}`, account: Converter.user(n.user, host), in_reply_to_id: n.replyId, in_reply_to_account_id: null, reblog: n.renote ? Converter.note(n.renote, host) : null, content: html, plain_content: n.text ? n.text : null, created_at: n.createdAt, edited_at: null, emojis: [...Converter.emojiConverter(n.emojis).map(e => Converter.emoji(e)), ...localEmojis], replies_count: n.repliesCount, reblogs_count: n.renoteCount, favourites_count: 0, reblogged: false, favourited: !!n.myReaction, muted: false, sensitive: n.files ? n.files.some(f => f.isSensitive) : false, spoiler_text: n.cw ? n.cw : '', visibility: Converter.visibility(n.visibility), media_attachments: 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(host, n.reactions, n.myReaction || '', n.reactionEmojis || {}), bookmarked: false, quote: !!(n.renote && n.text), quote_status: n.renote && n.text ? Converter.note(n.renote, host) : null, _integrated_isLocal: n._integrated_isLocal || false }; }; Converter.mapReactions = (host, r, myReaction, emojiData) => { if (isEmojiArr(emojiData)) { return emojiData.map(e => { return { count: 0, me: false, name: e.name, url: e.url, static_url: e.url }; }); } return Object.keys(r).map(key => { const keyObj = key.replace(/:/g, ''); const shortcode = key.replace(/[:@.]/g, ''); const isCustomEmoji = shortcode !== key; if (myReaction && key === myReaction) { return { count: r[key], me: true, name: shortcode, url: isCustomEmoji ? emojiData[keyObj] || `https://${host}/emoji/${shortcode}.webp` : undefined, static_url: isCustomEmoji ? emojiData[keyObj] || `https://${host}/emoji/${shortcode}.webp` : undefined }; } return { count: r[key], me: false, name: shortcode, url: isCustomEmoji ? emojiData[keyObj] || `https://${host}/emoji/${shortcode}.webp` : undefined, static_url: isCustomEmoji ? emojiData[keyObj] || `https://${host}/emoji/${shortcode}.webp` : undefined }; }); }; Converter.reactions = (r, host, emojiData) => { const result = []; if (isEmojiArr(emojiData)) { return emojiData.map(e => { return { count: 0, me: false, name: e.name, url: e.url, static_url: e.url }; }); } for (const e of r) { const shortcode = e.type.replace(/[:@.]/g, ''); const isCustomEmoji = shortcode !== e.type; const i = result.findIndex(res => res.name === e.type); if (i >= 0) { result[i].count++; } else { result.push({ count: 1, me: false, name: shortcode, url: isCustomEmoji ? emojiData[shortcode] || `https://${host}/emoji/${shortcode}.webp` : undefined, static_url: isCustomEmoji ? emojiData[shortcode] || `https://${host}/emoji/${shortcode}.webp` : undefined }); } } return result; }; Converter.emojiExtractor = (text, host) => { const r = text.match(/:([a-zA-Z0-9_+-]+):/g); if (!r) return []; const unique = Array.from(new Set(r)); return unique.map(u => { const shortcode = u.replace(/:/g, ''); return { name: shortcode, host: null, url: `https://${host}/emoji/${shortcode}.webp`, aliases: [] }; }); }; Converter.noteToConversation = (n, host) => { const accounts = [Converter.user(n.user, host)]; if (n.reply) { accounts.push(Converter.user(n.reply.user, host)); } return { id: n.id, accounts: accounts, last_status: Converter.note(n, host), unread: false }; }; Converter.list = (l) => ({ id: l.id, title: l.name, replies_policy: null }); Converter.antennaToList = (a) => ({ id: a.id, title: a.name, replies_policy: null, is_misskey_antenna: true }); Converter.encodeNotificationType = (e) => { switch (e) { case NotificationType.Follow: return MisskeyNotificationType.Follow; case NotificationType.Mention: return MisskeyNotificationType.Reply; case NotificationType.Favourite: case NotificationType.Reaction: return MisskeyNotificationType.Reaction; case NotificationType.Reblog: return MisskeyNotificationType.Renote; case NotificationType.PollVote: return MisskeyNotificationType.PollVote; case NotificationType.FollowRequest: return MisskeyNotificationType.ReceiveFollowRequest; default: return e; } }; Converter.decodeNotificationType = (e) => { switch (e) { case MisskeyNotificationType.Follow: return NotificationType.Follow; case MisskeyNotificationType.Mention: case MisskeyNotificationType.Reply: return NotificationType.Mention; case MisskeyNotificationType.Renote: case MisskeyNotificationType.Quote: return NotificationType.Reblog; case MisskeyNotificationType.Reaction: return NotificationType.Reaction; case MisskeyNotificationType.PollVote: return NotificationType.PollVote; case MisskeyNotificationType.ReceiveFollowRequest: return NotificationType.FollowRequest; case MisskeyNotificationType.FollowRequestAccepted: return NotificationType.Follow; default: return e; } }; const modelOfAcct = { id: '1', username: 'none', acct: 'none', display_name: 'none', locked: true, bot: true, discoverable: false, group: false, created_at: '1971-01-01T00:00:00.000Z', note: '', url: 'https://http.cat/404', avatar: 'https://http.cat/404', avatar_static: 'https://http.cat/404', header: 'https://http.cat/404', header_static: 'https://http.cat/404', followers_count: -1, following_count: 0, statuses_count: 0, noindex: true, emojis: [], fields: [], moved: null, suspended: false, limited: false }; Converter.notification = (n, host) => { let notification = { id: n.id, account: n.user ? Converter.user(n.user, host) : modelOfAcct, created_at: n.createdAt, type: Converter.decodeNotificationType(n.type) }; if (n.note) { notification = Object.assign(notification, { status: Converter.note(n.note, host) }); } if (n.reaction) { notification = Object.assign(notification, { emoji: n.reaction }); } 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, email: m.maintainerEmail, version: m.version, thumbnail: m.bannerUrl, urls: { streaming_api: `${wss}/streaming` }, stats: Converter.stats(s), languages: m.langs, contact_account: undefined, registrations: !m.disableRegistration, approval_required: false, configuration: { statuses: { max_characters: m.maxNoteTextLength } } }; }; Converter.hashtag = (h) => { return { name: h.tag, url: h.tag, history: [], following: false }; }; })(Converter = MisskeyAPI.Converter || (MisskeyAPI.Converter = {})); MisskeyAPI.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 post(path, params = {}, headers = {}) { const options = { headers: headers, maxContentLength: Infinity, maxBodyLength: Infinity }; let bodyParams = params; if (this.accessToken) { if (params instanceof FormData) { const limit = parseInt(bodyParams.get('limit'), 10); if (limit) bodyParams.set('limit', limit); bodyParams.append('i', this.accessToken); } else { params.limit = parseInt(params.limit, 10) || undefined; 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(channel, listId) { if (!this.accessToken) { throw new Error('accessToken is required'); } const url = this.baseUrl.replace('https://', 'wss://') + '/streaming'; const streaming = new WebSocket(url, channel, this.accessToken, listId, this.userAgent); if (!isBrowser()) { process.nextTick(() => { streaming.start(); }); } else { setTimeout(() => { streaming.start(); }, 0); } return streaming; } } MisskeyAPI.Client = Client; })(MisskeyAPI || (MisskeyAPI = {})); export default MisskeyAPI;