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