@cutls/megalodon
Version:
Mastodon, Pleroma, Misskey API client for node.js and browser
587 lines (586 loc) • 15.7 kB
JavaScript
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/`/g, '`').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