summaryrefslogtreecommitdiff
path: root/packages/megalodon/src/misskey/api_client.ts
diff options
context:
space:
mode:
authorMar0xy <marie@kaifa.ch>2023-09-25 01:20:03 +0200
committerMar0xy <marie@kaifa.ch>2023-09-25 01:20:03 +0200
commit89eea5df5286a9fdf2976d41cdbaf0c40dd19b62 (patch)
tree74081b1197c62768b7a012388e3c7eeb49ef30c6 /packages/megalodon/src/misskey/api_client.ts
parentfix: upload media on masto api (diff)
downloadsharkey-89eea5df5286a9fdf2976d41cdbaf0c40dd19b62.tar.gz
sharkey-89eea5df5286a9fdf2976d41cdbaf0c40dd19b62.tar.bz2
sharkey-89eea5df5286a9fdf2976d41cdbaf0c40dd19b62.zip
test: check old megalodon version
Diffstat (limited to 'packages/megalodon/src/misskey/api_client.ts')
-rw-r--r--packages/megalodon/src/misskey/api_client.ts1287
1 files changed, 688 insertions, 599 deletions
diff --git a/packages/megalodon/src/misskey/api_client.ts b/packages/megalodon/src/misskey/api_client.ts
index ead094aca0..e4dd140459 100644
--- a/packages/megalodon/src/misskey/api_client.ts
+++ b/packages/megalodon/src/misskey/api_client.ts
@@ -1,638 +1,727 @@
-import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
-import dayjs from 'dayjs'
-import FormData from 'form-data'
+import axios, { AxiosResponse, AxiosRequestConfig } from "axios";
+import dayjs from "dayjs";
+import FormData from "form-data";
-import { DEFAULT_UA } from '../default'
-import proxyAgent, { ProxyConfig } from '../proxy_config'
-import Response from '../response'
-import MisskeyEntity from './entity'
-import MegalodonEntity from '../entity'
-import WebSocket from './web_socket'
-import MisskeyNotificationType from './notification'
-import NotificationType, { UnknownNotificationTypeError } from '../notification'
+import { DEFAULT_UA } from "../default";
+import proxyAgent, { ProxyConfig } from "../proxy_config";
+import Response from "../response";
+import MisskeyEntity from "./entity";
+import MegalodonEntity from "../entity";
+import WebSocket from "./web_socket";
+import MisskeyNotificationType from "./notification";
+import NotificationType from "../notification";
namespace MisskeyAPI {
- export namespace Entity {
- export type Announcement = MisskeyEntity.Announcement
- export type App = MisskeyEntity.App
- export type Blocking = MisskeyEntity.Blocking
- export type Choice = MisskeyEntity.Choice
- export type CreatedNote = MisskeyEntity.CreatedNote
- export type Emoji = MisskeyEntity.Emoji
- export type Favorite = MisskeyEntity.Favorite
- export type File = MisskeyEntity.File
- export type Follower = MisskeyEntity.Follower
- export type Following = MisskeyEntity.Following
- export type FollowRequest = MisskeyEntity.FollowRequest
- export type Hashtag = MisskeyEntity.Hashtag
- export type List = MisskeyEntity.List
- export type Meta = MisskeyEntity.Meta
- export type Mute = MisskeyEntity.Mute
- export type Note = MisskeyEntity.Note
- export type Notification = MisskeyEntity.Notification
- export type Poll = MisskeyEntity.Poll
- export type Reaction = MisskeyEntity.Reaction
- export type Relation = MisskeyEntity.Relation
- export type User = MisskeyEntity.User
- export type UserDetail = MisskeyEntity.UserDetail
- export type UserKey = MisskeyEntity.UserKey
- export type Session = MisskeyEntity.Session
- export type Stats = MisskeyEntity.Stats
- }
+ export namespace Entity {
+ export type App = MisskeyEntity.App;
+ export type Announcement = MisskeyEntity.Announcement;
+ export type Blocking = MisskeyEntity.Blocking;
+ export type Choice = MisskeyEntity.Choice;
+ export type CreatedNote = MisskeyEntity.CreatedNote;
+ export type Emoji = MisskeyEntity.Emoji;
+ export type Favorite = MisskeyEntity.Favorite;
+ export type Field = MisskeyEntity.Field;
+ export type File = MisskeyEntity.File;
+ export type Follower = MisskeyEntity.Follower;
+ export type Following = MisskeyEntity.Following;
+ export type FollowRequest = MisskeyEntity.FollowRequest;
+ export type Hashtag = MisskeyEntity.Hashtag;
+ export type List = MisskeyEntity.List;
+ export type Meta = MisskeyEntity.Meta;
+ export type Mute = MisskeyEntity.Mute;
+ export type Note = MisskeyEntity.Note;
+ export type Notification = MisskeyEntity.Notification;
+ export type Poll = MisskeyEntity.Poll;
+ export type Reaction = MisskeyEntity.Reaction;
+ export type Relation = MisskeyEntity.Relation;
+ export type User = MisskeyEntity.User;
+ export type UserDetail = MisskeyEntity.UserDetail;
+ export type UserDetailMe = MisskeyEntity.UserDetailMe;
+ export type GetAll = MisskeyEntity.GetAll;
+ export type UserKey = MisskeyEntity.UserKey;
+ export type Session = MisskeyEntity.Session;
+ export type Stats = MisskeyEntity.Stats;
+ export type State = MisskeyEntity.State;
+ export type APIEmoji = { emojis: Emoji[] };
+ }
- export namespace Converter {
- export const announcement = (a: Entity.Announcement): MegalodonEntity.Announcement => ({
- 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: []
- })
+ export class Converter {
+ private baseUrl: string;
+ private instanceHost: string;
+ private plcUrl: string;
+ private 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: "plc",
+ avatar: "plc",
+ avatar_static: "plc",
+ header: "plc",
+ header_static: "plc",
+ followers_count: -1,
+ following_count: 0,
+ statuses_count: 0,
+ last_status_at: "1971-01-01T00:00:00.000Z",
+ noindex: true,
+ emojis: [],
+ fields: [],
+ moved: null,
+ };
- export const emoji = (e: Entity.Emoji): MegalodonEntity.Emoji => {
- return {
- shortcode: e.name,
- static_url: e.url,
- url: e.url,
- visible_in_picker: true,
- category: e.category
- }
- }
+ constructor(baseUrl: string) {
+ this.baseUrl = baseUrl;
+ this.instanceHost = baseUrl.substring(baseUrl.indexOf("//") + 2);
+ this.plcUrl = `${baseUrl}/static-assets/transparent.png`;
+ this.modelOfAcct.url = this.plcUrl;
+ this.modelOfAcct.avatar = this.plcUrl;
+ this.modelOfAcct.avatar_static = this.plcUrl;
+ this.modelOfAcct.header = this.plcUrl;
+ this.modelOfAcct.header_static = this.plcUrl;
+ }
- export const user = (u: Entity.User, host: string | null = null): MegalodonEntity.Account => {
- host ? host = host.replace("https://", "") : null;
- let acct = u.username
- if (u.host) {
- acct = `${u.username}@${u.host}`
- }
- if (host) {
- acct = `${u.username}@${host}`
- }
- return {
- id: u.id,
- username: u.username,
- acct: acct,
- display_name: u.name,
- locked: false,
- group: null,
- noindex: null,
- suspended: null,
- limited: null,
- created_at: '',
- followers_count: 0,
- following_count: 0,
- statuses_count: 0,
- note: '',
- url: acct,
- avatar: u.avatarUrl,
- avatar_static: u.avatarColor,
- header: '',
- header_static: '',
- emojis: mapEmojis(u.emojis),
- moved: null,
- fields: [],
- bot: null
- }
- }
+ // FIXME: Properly render MFM instead of just escaping HTML characters.
+ escapeMFM = (text: string): string =>
+ text
+ .replace(/&/g, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&#39;")
+ .replace(/`/g, "&#x60;")
+ .replace(/\r?\n/g, "<br>");
- export const userDetail = (u: Entity.UserDetail, host: string | null = null): MegalodonEntity.Account => {
- host ? host = host.replace("https://", "") : null;
- let acct = u.username
- if (u.host) {
- acct = `${u.username}@${u.host}`
- }
- if (host) {
- acct = `${u.username}@${host}`
- }
- return {
- id: u.id,
- username: u.username,
- acct: acct,
- display_name: u.name,
- locked: u.isLocked,
- group: null,
- noindex: null,
- suspended: null,
- limited: null,
- created_at: u.createdAt,
- followers_count: u.followersCount,
- following_count: u.followingCount,
- statuses_count: u.notesCount,
- note: u.description ? u.description : '',
- url: acct,
- avatar: u.avatarUrl,
- avatar_static: u.avatarColor,
- header: u.bannerUrl,
- header_static: u.bannerColor,
- emojis: mapEmojis(u.emojis),
- moved: null,
- fields: [],
- bot: u.isBot
- }
- }
+ emoji = (e: Entity.Emoji): MegalodonEntity.Emoji => {
+ return {
+ shortcode: e.name,
+ static_url: e.url,
+ url: e.url,
+ visible_in_picker: true,
+ category: e.category,
+ };
+ };
- export const visibility = (v: 'public' | 'home' | 'followers' | 'specified'): 'public' | 'unlisted' | 'private' | 'direct' => {
- switch (v) {
- case 'public':
- return v
- case 'home':
- return 'unlisted'
- case 'followers':
- return 'private'
- case 'specified':
- return 'direct'
- }
- }
+ field = (f: Entity.Field): MegalodonEntity.Field => ({
+ name: f.name,
+ value: this.escapeMFM(f.value),
+ verified_at: null,
+ });
- export const encodeVisibility = (v: 'public' | 'unlisted' | 'private' | 'direct'): 'public' | 'home' | 'followers' | 'specified' => {
- switch (v) {
- case 'public':
- return v
- case 'unlisted':
- return 'home'
- case 'private':
- return 'followers'
- case 'direct':
- return 'specified'
- }
- }
+ user = (u: Entity.User): MegalodonEntity.Account => {
+ let acct = u.username;
+ let acctUrl = `https://${u.host || this.instanceHost}/@${u.username}`;
+ if (u.host) {
+ acct = `${u.username}@${u.host}`;
+ acctUrl = `https://${u.host}/@${u.username}`;
+ }
+ 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: this.plcUrl,
+ header_static: this.plcUrl,
+ emojis: u.emojis && u.emojis.length > 0 ? u.emojis.map((e) => this.emoji(e)) : [],
+ moved: null,
+ fields: [],
+ bot: false,
+ };
+ };
- export const fileType = (s: string): 'unknown' | 'image' | 'gifv' | 'video' | 'audio' => {
- 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'
- }
+ userDetail = (
+ u: Entity.UserDetail,
+ host: string,
+ ): MegalodonEntity.Account => {
+ let acct = u.username;
+ host = host.replace("https://", "");
+ let acctUrl = `https://${host || u.host || this.instanceHost}/@${
+ u.username
+ }`;
+ if (u.host) {
+ acct = `${u.username}@${u.host}`;
+ acctUrl = `https://${u.host}/@${u.username}`;
+ }
+ return {
+ id: u.id,
+ username: u.username,
+ acct: acct,
+ display_name: u.name || u.username,
+ locked: u.isLocked,
+ created_at: u.createdAt,
+ followers_count: u.followersCount,
+ following_count: u.followingCount,
+ statuses_count: u.notesCount,
+ note: u.description?.replace(/\n|\\n/g, "<br>") ?? "",
+ url: acctUrl,
+ avatar: u.avatarUrl,
+ avatar_static: u.avatarUrl,
+ header: u.bannerUrl ?? this.plcUrl,
+ header_static: u.bannerUrl ?? this.plcUrl,
+ emojis: u.emojis && u.emojis.length > 0 ? u.emojis.map((e) => this.emoji(e)) : [],
+ moved: null,
+ fields: u.fields.map((f) => this.field(f)),
+ bot: u.isBot,
+ };
+ };
- export const file = (f: Entity.File): MegalodonEntity.Attachment => {
- return {
- id: f.id,
- type: 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
- }
- }
+ userPreferences = (
+ u: MisskeyAPI.Entity.UserDetailMe,
+ v: "public" | "unlisted" | "private" | "direct",
+ ): MegalodonEntity.Preferences => {
+ return {
+ "reading:expand:media": "default",
+ "reading:expand:spoilers": false,
+ "posting:default:language": u.lang,
+ "posting:default:sensitive": u.alwaysMarkNsfw,
+ "posting:default:visibility": v,
+ };
+ };
- export const follower = (f: Entity.Follower): MegalodonEntity.Account => {
- return user(f.follower)
- }
+ visibility = (
+ v: "public" | "home" | "followers" | "specified",
+ ): "public" | "unlisted" | "private" | "direct" => {
+ switch (v) {
+ case "public":
+ return v;
+ case "home":
+ return "unlisted";
+ case "followers":
+ return "private";
+ case "specified":
+ return "direct";
+ }
+ };
- export const following = (f: Entity.Following): MegalodonEntity.Account => {
- return user(f.followee)
- }
+ encodeVisibility = (
+ v: "public" | "unlisted" | "private" | "direct",
+ ): "public" | "home" | "followers" | "specified" => {
+ switch (v) {
+ case "public":
+ return v;
+ case "unlisted":
+ return "home";
+ case "private":
+ return "followers";
+ case "direct":
+ return "specified";
+ }
+ };
- export const relation = (r: Entity.Relation): MegalodonEntity.Relationship => {
- 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
- }
- }
+ fileType = (
+ s: string,
+ ): "unknown" | "image" | "gifv" | "video" | "audio" => {
+ 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";
+ };
- export const choice = (c: Entity.Choice): MegalodonEntity.PollOption => {
- return {
- title: c.text,
- votes_count: c.votes
- }
- }
+ file = (f: Entity.File): MegalodonEntity.Attachment => {
+ return {
+ id: f.id,
+ type: this.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: f.comment,
+ blurhash: f.blurhash,
+ };
+ };
- export const poll = (p: Entity.Poll): MegalodonEntity.Poll => {
- 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 => choice(c)) : [],
- voted: Array.isArray(p.choices) ? p.choices.some(c => c.isVoted) : false
- }
- }
+ follower = (f: Entity.Follower): MegalodonEntity.Account => {
+ return this.user(f.follower);
+ };
- export const note = (n: Entity.Note, host: string | null = null): MegalodonEntity.Status => {
- host ? host = host.replace("https://", "") : null;
- return {
- id: n.id,
- uri: n.uri ? n.uri : host ? `https://${host}/notes/${n.id}` : '',
- url: n.url ? n.url : host ? `https://${host}/notes/${n.id}` : '',
- account: user(n.user, host ? host : null),
- in_reply_to_id: n.replyId,
- in_reply_to_account_id: null,
- reblog: n.renote ? 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,
- emojis: mapEmojis(n.emojis).concat(mapReactionEmojis(n.reactionEmojis)),
- replies_count: n.repliesCount,
- reblogs_count: n.renoteCount,
- favourites_count: 0,
- reblogged: false,
- favourited: false,
- muted: false,
- sensitive: Array.isArray(n.files) ? n.files.some(f => f.isSensitive) : false,
- spoiler_text: n.cw ? n.cw : '',
- visibility: visibility(n.visibility),
- media_attachments: Array.isArray(n.files) ? n.files.map(f => file(f)) : [],
- mentions: [],
- tags: [],
- card: null,
- poll: n.poll ? poll(n.poll) : null,
- application: null,
- language: null,
- pinned: null,
- emoji_reactions: typeof n.reactions === 'object' ? mapReactions(n.reactions, n.myReaction) : [],
- bookmarked: false,
- quote: n.renote !== undefined && n.text !== null
- }
- }
+ following = (f: Entity.Following): MegalodonEntity.Account => {
+ return this.user(f.followee);
+ };
- const mapEmojis = (e: Array<Entity.Emoji> | { [key: string]: string }): Array<MegalodonEntity.Emoji> => {
- if (Array.isArray(e)) {
- return e.map(e => emoji(e))
- } else if (e) {
- return mapReactionEmojis(e)
- } else {
- return []
- }
- }
+ relation = (r: Entity.Relation): MegalodonEntity.Relationship => {
+ 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,
+ };
+ };
- export const mapReactions = (r: { [key: string]: number }, myReaction?: string): Array<MegalodonEntity.Reaction> => {
- return Object.keys(r).map(key => {
- if (myReaction && key === myReaction) {
- return {
- count: r[key],
- me: true,
- name: key
- }
- }
- return {
- count: r[key],
- me: false,
- name: key
- }
- })
- }
+ choice = (c: Entity.Choice): MegalodonEntity.PollOption => {
+ return {
+ title: c.text,
+ votes_count: c.votes,
+ };
+ };
- const mapReactionEmojis = (r: { [key: string]: string }): Array<MegalodonEntity.Emoji> => {
- return Object.keys(r).map(key => ({
- shortcode: key,
- static_url: r[key],
- url: r[key],
- visible_in_picker: true,
- category: ''
- }))
- }
+ poll = (p: Entity.Poll, id: string): MegalodonEntity.Poll => {
+ const now = dayjs();
+ const expire = dayjs(p.expiresAt);
+ const count = p.choices.reduce((sum, choice) => sum + choice.votes, 0);
+ return {
+ id: id,
+ expires_at: p.expiresAt,
+ expired: now.isAfter(expire),
+ multiple: p.multiple,
+ votes_count: count,
+ options: p.choices.map((c) => this.choice(c)),
+ voted: p.choices.some((c) => c.isVoted),
+ own_votes: p.choices
+ .filter((c) => c.isVoted)
+ .map((c) => p.choices.indexOf(c)),
+ };
+ };
- export const reactions = (r: Array<Entity.Reaction>): Array<MegalodonEntity.Reaction> => {
- const result: Array<MegalodonEntity.Reaction> = []
- 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
- }
+ note = (n: Entity.Note, host: string): MegalodonEntity.Status => {
+ host = host.replace("https://", "");
- export const noteToConversation = (n: Entity.Note): MegalodonEntity.Conversation => {
- const accounts: Array<MegalodonEntity.Account> = [user(n.user)]
- if (n.reply) {
- accounts.push(user(n.reply.user))
- }
- return {
- id: n.id,
- accounts: accounts,
- last_status: note(n),
- unread: false
- }
- }
+ 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: this.user(n.user),
+ in_reply_to_id: n.replyId,
+ in_reply_to_account_id: n.reply?.userId ?? null,
+ reblog: n.renote ? this.note(n.renote, host) : null,
+ content: n.text ? this.escapeMFM(n.text) : "",
+ plain_content: n.text ? n.text : null,
+ created_at: n.createdAt,
+ // Remove reaction emojis with names containing @ from the emojis list.
+ emojis: n.emojis && n.emojis.length > 0 ? n.emojis
+ .filter((e) => e.name.indexOf("@") === -1)
+ .map((e) => this.emoji(e)) : [],
+ replies_count: n.repliesCount,
+ reblogs_count: n.renoteCount,
+ favourites_count: this.getTotalReactions(n.reactions),
+ reblogged: false,
+ favourited: !!n.myReaction,
+ muted: false,
+ sensitive: n.files ? n.files.some((f) => f.isSensitive) : false,
+ spoiler_text: n.cw ? n.cw : "",
+ visibility: this.visibility(n.visibility),
+ media_attachments: n.files ? n.files.map((f) => this.file(f)) : [],
+ mentions: [],
+ tags: [],
+ card: null,
+ poll: n.poll ? this.poll(n.poll, n.id) : null,
+ application: null,
+ language: null,
+ pinned: null,
+ // Use emojis list to provide URLs for emoji reactions.
+ reactions: n.emojis && n.emojis.length > 0 ? this.mapReactions(n.emojis, n.reactions, n.myReaction) : [],
+ bookmarked: false,
+ quote: n.renote && n.text ? this.note(n.renote, host) : null,
+ };
+ };
- export const list = (l: Entity.List): MegalodonEntity.List => ({
- id: l.id,
- title: l.name,
- replies_policy: null
- })
+ mapReactions = (
+ emojis: Array<MisskeyEntity.Emoji>,
+ r: { [key: string]: number },
+ myReaction?: string,
+ ): Array<MegalodonEntity.Reaction> => {
+ // Map of emoji shortcodes to image URLs.
+ const emojiUrls = new Map<string, string>(
+ emojis.map((e) => [e.name, e.url]),
+ );
+ return Object.keys(r).map((key) => {
+ // Strip colons from custom emoji reaction names to match emoji shortcodes.
+ const shortcode = key.replaceAll(":", "");
+ // If this is a custom emoji (vs. a Unicode emoji), find its image URL.
+ const url = emojiUrls.get(shortcode);
+ // Finally, remove trailing @. from local custom emoji reaction names.
+ const name = shortcode.replace("@.", "");
+ return {
+ count: r[key],
+ me: key === myReaction,
+ name,
+ url,
+ // We don't actually have a static version of the asset, but clients expect one anyway.
+ static_url: url,
+ };
+ });
+ };
- export const encodeNotificationType = (
- e: MegalodonEntity.NotificationType
- ): MisskeyEntity.NotificationType | UnknownNotificationTypeError => {
- switch (e) {
- case NotificationType.Follow:
- return MisskeyNotificationType.Follow
- case NotificationType.Mention:
- return MisskeyNotificationType.Reply
- case NotificationType.Favourite:
- case NotificationType.EmojiReaction:
- return MisskeyNotificationType.Reaction
- case NotificationType.Reblog:
- return MisskeyNotificationType.Renote
- case NotificationType.PollVote:
- return MisskeyNotificationType.PollVote
- case NotificationType.FollowRequest:
- return MisskeyNotificationType.ReceiveFollowRequest
- default:
- return new UnknownNotificationTypeError()
- }
- }
+ getTotalReactions = (r: { [key: string]: number }): number => {
+ return Object.values(r).length > 0
+ ? Object.values(r).reduce(
+ (previousValue, currentValue) => previousValue + currentValue,
+ )
+ : 0;
+ };
- export const decodeNotificationType = (
- e: MisskeyEntity.NotificationType
- ): MegalodonEntity.NotificationType | UnknownNotificationTypeError => {
- 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.EmojiReaction
- case MisskeyNotificationType.PollVote:
- return NotificationType.PollVote
- case MisskeyNotificationType.ReceiveFollowRequest:
- return NotificationType.FollowRequest
- case MisskeyNotificationType.FollowRequestAccepted:
- return NotificationType.Follow
- default:
- return new UnknownNotificationTypeError()
- }
- }
+ reactions = (
+ r: Array<Entity.Reaction>,
+ ): Array<MegalodonEntity.Reaction> => {
+ const result: Array<MegalodonEntity.Reaction> = [];
+ for (const e of r) {
+ 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;
+ };
- export const notification = (n: Entity.Notification): MegalodonEntity.Notification | UnknownNotificationTypeError => {
- const notificationType = decodeNotificationType(n.type)
- if (notificationType instanceof UnknownNotificationTypeError) {
- return notificationType
- }
- let notification = {
- id: n.id,
- account: user(n.user),
- created_at: n.createdAt,
- type: notificationType
- }
- if (n.note) {
- notification = Object.assign(notification, {
- status: note(n.note)
- })
- }
- if (n.reaction) {
- notification = Object.assign(notification, {
- emoji: n.reaction
- })
- }
- return notification
- }
+ noteToConversation = (
+ n: Entity.Note,
+ host: string,
+ ): MegalodonEntity.Conversation => {
+ const accounts: Array<MegalodonEntity.Account> = [this.user(n.user)];
+ if (n.reply) {
+ accounts.push(this.user(n.reply.user));
+ }
+ return {
+ id: n.id,
+ accounts: accounts,
+ last_status: this.note(n, host),
+ unread: false,
+ };
+ };
- export const stats = (s: Entity.Stats): MegalodonEntity.Stats => {
- return {
- user_count: s.usersCount,
- status_count: s.notesCount,
- domain_count: s.instances
- }
- }
+ list = (l: Entity.List): MegalodonEntity.List => ({
+ id: l.id,
+ title: l.name,
+ });
- export const meta = (m: Entity.Meta, s: Entity.Stats): MegalodonEntity.Instance => {
- 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: stats(s),
- languages: m.langs,
- registrations: !m.disableRegistration,
- approval_required: false,
- configuration: {
- statuses: {
- max_characters: m.maxNoteTextLength,
- max_media_attachments: m.policies.clipLimit
- }
- }
- }
- }
+ encodeNotificationType = (
+ e: MegalodonEntity.NotificationType,
+ ): MisskeyEntity.NotificationType => {
+ 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.Poll:
+ return MisskeyNotificationType.PollEnded;
+ case NotificationType.FollowRequest:
+ return MisskeyNotificationType.ReceiveFollowRequest;
+ default:
+ return e;
+ }
+ };
- export const hashtag = (h: Entity.Hashtag): MegalodonEntity.Tag => {
- return {
- name: h.tag,
- url: h.tag,
- history: [],
- following: false
- }
- }
- }
+ decodeNotificationType = (
+ e: MisskeyEntity.NotificationType,
+ ): MegalodonEntity.NotificationType => {
+ 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.PollEnded:
+ return NotificationType.Poll;
+ case MisskeyNotificationType.ReceiveFollowRequest:
+ return NotificationType.FollowRequest;
+ case MisskeyNotificationType.FollowRequestAccepted:
+ return NotificationType.Follow;
+ default:
+ return e;
+ }
+ };
- export const 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'
- ]
+ announcement = (a: Entity.Announcement): MegalodonEntity.Announcement => ({
+ id: a.id,
+ content: `<h1>${this.escapeMFM(a.title)}</h1>${this.escapeMFM(a.text)}`,
+ starts_at: null,
+ ends_at: null,
+ published: true,
+ all_day: false,
+ published_at: a.createdAt,
+ updated_at: a.updatedAt,
+ read: a.isRead,
+ mentions: [],
+ statuses: [],
+ tags: [],
+ emojis: [],
+ reactions: [],
+ });
- /**
- * Interface
- */
- export interface Interface {
- get<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>>
- post<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>>
- cancel(): void
- socket(channel: 'user' | 'localTimeline' | 'hybridTimeline' | 'globalTimeline' | 'conversation' | 'list', listId?: string): WebSocket
- }
+ notification = (
+ n: Entity.Notification,
+ host: string,
+ ): MegalodonEntity.Notification => {
+ let notification = {
+ id: n.id,
+ account: n.user ? this.user(n.user) : this.modelOfAcct,
+ created_at: n.createdAt,
+ type: this.decodeNotificationType(n.type),
+ };
+ if (n.note) {
+ notification = Object.assign(notification, {
+ status: this.note(n.note, host),
+ });
+ if (notification.type === NotificationType.Poll) {
+ notification = Object.assign(notification, {
+ account: this.note(n.note, host).account,
+ });
+ }
+ if (n.reaction) {
+ notification = Object.assign(notification, {
+ reaction: this.mapReactions(n.note.emojis, { [n.reaction]: 1 })[0],
+ });
+ }
+ }
+ return notification;
+ };
- /**
- * Misskey API client.
- *
- * Usign axios for request, you will handle promises.
- */
- export class Client implements Interface {
- private accessToken: string | null
- private baseUrl: string
- private userAgent: string
- private abortController: AbortController
- private proxyConfig: ProxyConfig | false = false
+ stats = (s: Entity.Stats): MegalodonEntity.Stats => {
+ return {
+ user_count: s.usersCount,
+ status_count: s.notesCount,
+ domain_count: s.instances,
+ };
+ };
- /**
- * @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, userAgent: string = DEFAULT_UA, proxyConfig: ProxyConfig | false = false) {
- this.accessToken = accessToken
- this.baseUrl = baseUrl
- this.userAgent = userAgent
- this.proxyConfig = proxyConfig
- this.abortController = new AbortController()
- axios.defaults.signal = this.abortController.signal
- }
+ meta = (m: Entity.Meta, s: Entity.Stats): MegalodonEntity.Instance => {
+ 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: this.stats(s),
+ languages: m.langs,
+ contact_account: null,
+ max_toot_chars: m.maxNoteTextLength,
+ registrations: !m.disableRegistration,
+ };
+ };
- /**
- * GET request to misskey API.
- **/
- public async get<T>(path: string, params: any = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
- let options: AxiosRequestConfig = {
- params: params,
- headers: headers,
- maxContentLength: Infinity,
- maxBodyLength: Infinity
- }
- if (this.proxyConfig) {
- options = Object.assign(options, {
- httpAgent: proxyAgent(this.proxyConfig),
- httpsAgent: proxyAgent(this.proxyConfig)
- })
- }
- return axios.get<T>(this.baseUrl + path, options).then((resp: AxiosResponse<T>) => {
- const res: Response<T> = {
- data: resp.data,
- status: resp.status,
- statusText: resp.statusText,
- headers: resp.headers
- }
- return res
- })
- }
+ hashtag = (h: Entity.Hashtag): MegalodonEntity.Tag => {
+ return {
+ name: h.tag,
+ url: h.tag,
+ history: null,
+ following: false,
+ };
+ };
+ }
- /**
- * POST request to misskey REST API.
- * @param path relative path from baseUrl
- * @param params Form data
- * @param headers Request header object
- */
- public async post<T>(path: string, params: any = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
- let options: AxiosRequestConfig = {
- headers: headers,
- maxContentLength: Infinity,
- maxBodyLength: Infinity
- }
- if (this.proxyConfig) {
- options = Object.assign(options, {
- httpAgent: proxyAgent(this.proxyConfig),
- httpsAgent: proxyAgent(this.proxyConfig)
- })
- }
- 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<T>(this.baseUrl + path, bodyParams, options).then((resp: AxiosResponse<T>) => {
- const res: Response<T> = {
- data: resp.data,
- status: resp.status,
- statusText: resp.statusText,
- headers: resp.headers
- }
- return res
- })
- }
+ export const 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",
+ ];
- /**
- * Cancel all requests in this instance.
- * @returns void
- */
- public cancel(): void {
- return this.abortController.abort()
- }
+ /**
+ * Interface
+ */
+ export interface Interface {
+ post<T = any>(
+ path: string,
+ params?: any,
+ headers?: { [key: string]: string },
+ ): Promise<Response<T>>;
+ cancel(): void;
+ socket(
+ channel:
+ | "user"
+ | "localTimeline"
+ | "hybridTimeline"
+ | "globalTimeline"
+ | "conversation"
+ | "list",
+ listId?: string,
+ ): WebSocket;
+ }
- /**
- * Get connection and receive websocket connection for Misskey API.
- *
- * @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list.
- * @param listId This parameter is required only list channel.
- */
- public socket(
- channel: 'user' | 'localTimeline' | 'hybridTimeline' | 'globalTimeline' | 'conversation' | 'list',
- listId?: string
- ): WebSocket {
- if (!this.accessToken) {
- throw new Error('accessToken is required')
- }
- const url = this.baseUrl + '/streaming'
- const streaming = new WebSocket(url, channel, this.accessToken, listId, this.userAgent, this.proxyConfig)
- process.nextTick(() => {
- streaming.start()
- })
- return streaming
- }
- }
+ /**
+ * Misskey API client.
+ *
+ * Usign axios for request, you will handle promises.
+ */
+ export class Client implements Interface {
+ private accessToken: string | null;
+ private baseUrl: string;
+ private userAgent: string;
+ private abortController: AbortController;
+ private proxyConfig: ProxyConfig | false = false;
+ private converter: Converter;
+
+ /**
+ * @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.
+ * @param converter Converter instance.
+ */
+ constructor(
+ baseUrl: string,
+ accessToken: string | null,
+ userAgent: string = DEFAULT_UA,
+ proxyConfig: ProxyConfig | false = false,
+ converter: Converter,
+ ) {
+ this.accessToken = accessToken;
+ this.baseUrl = baseUrl;
+ this.userAgent = userAgent;
+ this.proxyConfig = proxyConfig;
+ this.abortController = new AbortController();
+ this.converter = converter;
+ axios.defaults.signal = this.abortController.signal;
+ }
+
+ /**
+ * POST request to mastodon REST API.
+ * @param path relative path from baseUrl
+ * @param params Form data
+ * @param headers Request header object
+ */
+ public async post<T>(
+ path: string,
+ params: any = {},
+ headers: { [key: string]: string } = {},
+ ): Promise<Response<T>> {
+ let options: AxiosRequestConfig = {
+ headers: headers,
+ maxContentLength: Infinity,
+ maxBodyLength: Infinity,
+ };
+ if (this.proxyConfig) {
+ options = Object.assign(options, {
+ httpAgent: proxyAgent(this.proxyConfig),
+ httpsAgent: proxyAgent(this.proxyConfig),
+ });
+ }
+ 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<T>(this.baseUrl + path, bodyParams, options)
+ .then((resp: AxiosResponse<T>) => {
+ const res: Response<T> = {
+ data: resp.data,
+ status: resp.status,
+ statusText: resp.statusText,
+ headers: resp.headers,
+ };
+ return res;
+ });
+ }
+
+ /**
+ * Cancel all requests in this instance.
+ * @returns void
+ */
+ public cancel(): void {
+ return this.abortController.abort();
+ }
+
+ /**
+ * Get connection and receive websocket connection for Misskey API.
+ *
+ * @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list.
+ * @param listId This parameter is required only list channel.
+ */
+ public socket(
+ channel:
+ | "user"
+ | "localTimeline"
+ | "hybridTimeline"
+ | "globalTimeline"
+ | "conversation"
+ | "list",
+ listId?: string,
+ ): WebSocket {
+ if (!this.accessToken) {
+ throw new Error("accessToken is required");
+ }
+ const url = `${this.baseUrl}/streaming`;
+ const streaming = new WebSocket(
+ url,
+ channel,
+ this.accessToken,
+ listId,
+ this.userAgent,
+ this.proxyConfig,
+ this.converter,
+ );
+ process.nextTick(() => {
+ streaming.start();
+ });
+ return streaming;
+ }
+ }
}
-export default MisskeyAPI
+export default MisskeyAPI;