From 332371c4819cd1b022877ef5a1e43a155e6ac277 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sun, 2 Jul 2023 18:58:12 +0200 Subject: [PATCH] Various fixes & refactoring --- megalodon/src/misskey.ts | 156 ++++++------- megalodon/src/misskey/api_client.ts | 205 ++++++++++-------- megalodon/src/misskey/web_socket.ts | 11 +- .../test/unit/misskey/api_client.spec.ts | 12 +- 4 files changed, 209 insertions(+), 175 deletions(-) diff --git a/megalodon/src/misskey.ts b/megalodon/src/misskey.ts index ec990e6..895e72f 100644 --- a/megalodon/src/misskey.ts +++ b/megalodon/src/misskey.ts @@ -10,6 +10,7 @@ import { MegalodonInterface, WebSocketInterface, NoImplementedError, ArgumentErr export default class Misskey implements MegalodonInterface { public client: MisskeyAPI.Interface + public converter: MisskeyAPI.Converter public baseUrl: string public proxyConfig: ProxyConfig | false @@ -33,7 +34,8 @@ export default class Misskey implements MegalodonInterface { if (userAgent) { agent = userAgent } - this.client = new MisskeyAPI.Client(baseUrl, token, agent, proxyConfig) + this.converter = new MisskeyAPI.Converter(baseUrl) + this.client = new MisskeyAPI.Client(baseUrl, token, agent, proxyConfig, this.converter) this.baseUrl = baseUrl this.proxyConfig = proxyConfig } @@ -203,7 +205,7 @@ export default class Misskey implements MegalodonInterface { public async verifyAccountCredentials(): Promise> { return this.client.post('/api/i').then(res => { return Object.assign(res, { - data: MisskeyAPI.Converter.userDetail(res.data, this.baseUrlToHost(this.baseUrl)) + data: this.converter.userDetail(res.data, this.baseUrlToHost(this.baseUrl)) }) }) } @@ -263,7 +265,7 @@ export default class Misskey implements MegalodonInterface { } return this.client.post('/api/i', params).then(res => { return Object.assign(res, { - data: MisskeyAPI.Converter.userDetail(res.data, this.baseUrlToHost(this.baseUrl)) + data: this.converter.userDetail(res.data, this.baseUrlToHost(this.baseUrl)) }) }) } @@ -278,7 +280,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => { return Object.assign(res, { - data: MisskeyAPI.Converter.userDetail(res.data, this.baseUrlToHost(this.baseUrl)) + data: this.converter.userDetail(res.data, this.baseUrlToHost(this.baseUrl)) }) }) } @@ -305,7 +307,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => { if (res.data.pinnedNotes) { - return { ...res, data: res.data.pinnedNotes.map(n => MisskeyAPI.Converter.note(n, this.baseUrlToHost(this.baseUrl))) } + return { ...res, data: res.data.pinnedNotes.map(n => this.converter.note(n, this.baseUrlToHost(this.baseUrl))) } } return { ...res, data: [] } }) @@ -347,7 +349,7 @@ export default class Misskey implements MegalodonInterface { } } return this.client.post>('/api/users/notes', params).then(res => { - const statuses: Array = res.data.map(note => MisskeyAPI.Converter.note(note, this.baseUrlToHost(this.baseUrl))) + const statuses: Array = res.data.map(note => this.converter.note(note, this.baseUrlToHost(this.baseUrl))) return Object.assign(res, { data: statuses }) @@ -384,7 +386,7 @@ export default class Misskey implements MegalodonInterface { } return this.client.post>('/api/users/reactions', params).then(res => { return Object.assign(res, { - data: res.data.map(fav => MisskeyAPI.Converter.note(fav.note, this.baseUrlToHost(this.baseUrl))) + data: res.data.map(fav => this.converter.note(fav.note, this.baseUrlToHost(this.baseUrl))) }) }) } @@ -426,7 +428,7 @@ export default class Misskey implements MegalodonInterface { } return this.client.post>('/api/users/followers', params).then(res => { return Object.assign(res, { - data: res.data.map(f => MisskeyAPI.Converter.follower(f)) + data: res.data.map(f => this.converter.follower(f)) }) }) } @@ -454,7 +456,7 @@ export default class Misskey implements MegalodonInterface { } return this.client.post>('/api/users/following', params).then(res => { return Object.assign(res, { - data: res.data.map(f => MisskeyAPI.Converter.following(f)) + data: res.data.map(f => this.converter.following(f)) }) }) } @@ -486,7 +488,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => { return Object.assign(res, { - data: MisskeyAPI.Converter.relation(res.data) + data: this.converter.relation(res.data) }) }) } @@ -504,7 +506,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => { return Object.assign(res, { - data: MisskeyAPI.Converter.relation(res.data) + data: this.converter.relation(res.data) }) }) } @@ -522,7 +524,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => { return Object.assign(res, { - data: MisskeyAPI.Converter.relation(res.data) + data: this.converter.relation(res.data) }) }) } @@ -540,7 +542,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => { return Object.assign(res, { - data: MisskeyAPI.Converter.relation(res.data) + data: this.converter.relation(res.data) }) }) } @@ -558,7 +560,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => { return Object.assign(res, { - data: MisskeyAPI.Converter.relation(res.data) + data: this.converter.relation(res.data) }) }) } @@ -576,7 +578,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => { return Object.assign(res, { - data: MisskeyAPI.Converter.relation(res.data) + data: this.converter.relation(res.data) }) }) } @@ -607,7 +609,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => { return Object.assign(res, { - data: MisskeyAPI.Converter.relation(res.data) + data: this.converter.relation(res.data) }) }) } @@ -655,7 +657,7 @@ export default class Misskey implements MegalodonInterface { } return this.client.post>('/api/users/search', params).then(res => { return Object.assign(res, { - data: res.data.map(u => MisskeyAPI.Converter.userDetail(u, this.baseUrlToHost(this.baseUrl))) + data: res.data.map(u => this.converter.userDetail(u, this.baseUrlToHost(this.baseUrl))) }) }) } @@ -692,7 +694,7 @@ export default class Misskey implements MegalodonInterface { } return this.client.post>('/api/i/favorites', params).then(res => { return Object.assign(res, { - data: res.data.map(s => MisskeyAPI.Converter.note(s.note, this.baseUrlToHost(this.baseUrl))) + data: res.data.map(s => this.converter.note(s.note, this.baseUrlToHost(this.baseUrl))) }) }) } @@ -732,7 +734,7 @@ export default class Misskey implements MegalodonInterface { } return this.client.post>('/api/mute/list', params).then(res => { return Object.assign(res, { - data: res.data.map(mute => MisskeyAPI.Converter.userDetail(mute.mutee, this.baseUrlToHost(this.baseUrl))) + data: res.data.map(mute => this.converter.userDetail(mute.mutee, this.baseUrlToHost(this.baseUrl))) }) }) } @@ -764,7 +766,7 @@ export default class Misskey implements MegalodonInterface { } return this.client.post>('/api/blocking/list', params).then(res => { return Object.assign(res, { - data: res.data.map(blocking => MisskeyAPI.Converter.userDetail(blocking.blockee, this.baseUrlToHost(this.baseUrl))) + data: res.data.map(blocking => this.converter.userDetail(blocking.blockee, this.baseUrlToHost(this.baseUrl))) }) }) } @@ -889,7 +891,7 @@ export default class Misskey implements MegalodonInterface { public async getFollowRequests(_limit?: number): Promise>> { return this.client.post>('/api/following/requests/list').then(res => { return Object.assign(res, { - data: res.data.map(r => MisskeyAPI.Converter.user(r.follower)) + data: res.data.map(r => this.converter.user(r.follower)) }) }) } @@ -907,7 +909,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => { return Object.assign(res, { - data: MisskeyAPI.Converter.relation(res.data) + data: this.converter.relation(res.data) }) }) } @@ -925,7 +927,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => { return Object.assign(res, { - data: MisskeyAPI.Converter.relation(res.data) + data: this.converter.relation(res.data) }) }) } @@ -994,7 +996,7 @@ export default class Misskey implements MegalodonInterface { scope: ['client', 'base'], }).then(ga => { return Object.assign(res, { - data: MisskeyAPI.Converter.userPreferences(res.data, ga.data) + data: this.converter.userPreferences(res.data, ga.data) }) }) */ @@ -1002,7 +1004,7 @@ export default class Misskey implements MegalodonInterface { // TODO: // FIXME: get this from api return Object.assign(res, { - data: MisskeyAPI.Converter.userPreferences(res.data, {defaultNoteVisibility: "followers", tutorial: -1}) + data: this.converter.userPreferences(res.data, {defaultNoteVisibility: "followers", tutorial: -1}) }) }) } @@ -1022,7 +1024,7 @@ export default class Misskey implements MegalodonInterface { } return this.client .post>('/api/users/recommendation', params) - .then(res => ({ ...res, data: res.data.map(u => MisskeyAPI.Converter.userDetail(u, this.baseUrlToHost(this.baseUrl))) })) + .then(res => ({ ...res, data: res.data.map(u => this.converter.userDetail(u, this.baseUrlToHost(this.baseUrl))) })) } // ====================================== @@ -1107,7 +1109,7 @@ export default class Misskey implements MegalodonInterface { } if (options.visibility) { params = Object.assign(params, { - visibility: MisskeyAPI.Converter.encodeVisibility(options.visibility) + visibility: this.converter.encodeVisibility(options.visibility) }) } if (options.quote_id) { @@ -1118,7 +1120,7 @@ export default class Misskey implements MegalodonInterface { } return this.client .post('/api/notes/create', params) - .then(res => ({ ...res, data: MisskeyAPI.Converter.note(res.data.createdNote, this.baseUrlToHost(this.baseUrl)) })) + .then(res => ({ ...res, data: this.converter.note(res.data.createdNote, this.baseUrlToHost(this.baseUrl)) })) } /** @@ -1129,7 +1131,7 @@ export default class Misskey implements MegalodonInterface { .post('/api/notes/show', { noteId: id }) - .then(res => ({ ...res, data: MisskeyAPI.Converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) + .then(res => ({ ...res, data: this.converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) } public async editStatus( @@ -1197,25 +1199,25 @@ export default class Misskey implements MegalodonInterface { depth: 12 }) } - return this.client.post>('/api/notes/children', params).then(res => { + return this.client.post>('/api/notes/children', params).then(async res => { console.log(JSON.stringify(res, null, 2)); - const parent : Entity.Status[] = []; + const parents: Entity.Status[] = []; + let lastNote = await this.getStatus(params.noteId); - this.getStatus(params.noteId).then(res => { - console.log("getting status"); - if (res.data.in_reply_to_id != null){ - console.log("replyid not null"); - this.getStatus(res.data.in_reply_to_id).then(res => { - console.log("setting parent to " + res.data.id); - parent.push(res.data); - }); - } - }); + for (let i = 0; i < 10; i++) { + const parentId = this.getParent(lastNote.data); + if (parentId == null) + break; + + const parent = await this.getStatus(parentId); + parents.push(parent.data); + lastNote = parent; + } const context: Entity.Context = { - ancestors: parent, - descendants: this.dfs(res.data.map(n => MisskeyAPI.Converter.note(n, this.baseUrlToHost(this.baseUrl)))) + ancestors: parents.reverse(), + descendants: this.dfs(res.data.map(n => this.converter.note(n, this.baseUrlToHost(this.baseUrl)))) } return { ...res, @@ -1224,6 +1226,10 @@ export default class Misskey implements MegalodonInterface { }) } + private getParent(note: Entity.Status) { + return note.in_reply_to_id ?? null; + } + private dfs(graph: Entity.Status[]) { // we don't need to run dfs if we have zero or one elements if (graph.length <= 1) { @@ -1274,7 +1280,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => ({ ...res, - data: res.data.map(n => MisskeyAPI.Converter.user(n.user)) + data: res.data.map(n => this.converter.user(n.user)) })) } @@ -1315,7 +1321,7 @@ export default class Misskey implements MegalodonInterface { .post('/api/notes/create', { renoteId: id }) - .then(res => ({ ...res, data: MisskeyAPI.Converter.note(res.data.createdNote, this.baseUrlToHost(this.baseUrl)) })) + .then(res => ({ ...res, data: this.converter.note(res.data.createdNote, this.baseUrlToHost(this.baseUrl)) })) } /** @@ -1329,7 +1335,7 @@ export default class Misskey implements MegalodonInterface { .post('/api/notes/show', { noteId: id }) - .then(res => ({ ...res, data: MisskeyAPI.Converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) + .then(res => ({ ...res, data: this.converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) } /** @@ -1343,7 +1349,7 @@ export default class Misskey implements MegalodonInterface { .post('/api/notes/show', { noteId: id }) - .then(res => ({ ...res, data: MisskeyAPI.Converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) + .then(res => ({ ...res, data: this.converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) } /** @@ -1357,7 +1363,7 @@ export default class Misskey implements MegalodonInterface { .post('/api/notes/show', { noteId: id }) - .then(res => ({ ...res, data: MisskeyAPI.Converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) + .then(res => ({ ...res, data: this.converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) } public async muteStatus(_id: string): Promise> { @@ -1385,7 +1391,7 @@ export default class Misskey implements MegalodonInterface { .post('/api/notes/show', { noteId: id }) - .then(res => ({ ...res, data: MisskeyAPI.Converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) + .then(res => ({ ...res, data: this.converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) } /** @@ -1399,7 +1405,7 @@ export default class Misskey implements MegalodonInterface { .post('/api/notes/show', { noteId: id }) - .then(res => ({ ...res, data: MisskeyAPI.Converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) + .then(res => ({ ...res, data: this.converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) } // ====================================== @@ -1420,12 +1426,12 @@ export default class Misskey implements MegalodonInterface { } return this.client .post('/api/drive/files/create', formData, headers) - .then(res => ({ ...res, data: MisskeyAPI.Converter.file(res.data) })) + .then(res => ({ ...res, data: this.converter.file(res.data) })) } public async getMedia(id: string): Promise> { const res = await this.client.post('/api/drive/files/show', { fileId: id }) - return { ...res, data: MisskeyAPI.Converter.file(res.data) } + return { ...res, data: this.converter.file(res.data) } } /** @@ -1452,7 +1458,7 @@ export default class Misskey implements MegalodonInterface { } return this.client .post('/api/drive/files/update', params) - .then(res => ({ ...res, data: MisskeyAPI.Converter.file(res.data) })) + .then(res => ({ ...res, data: this.converter.file(res.data) })) } // ====================================== @@ -1485,7 +1491,7 @@ export default class Misskey implements MegalodonInterface { noteId: status_id }) .then(res => { - const note = MisskeyAPI.Converter.note(res.data, this.baseUrlToHost(this.baseUrl)) + const note = this.converter.note(res.data, this.baseUrlToHost(this.baseUrl)) return { ...res, data: note.poll } }) if (!res.data) { @@ -1576,7 +1582,7 @@ export default class Misskey implements MegalodonInterface { } return this.client .post>('/api/notes/global-timeline', params) - .then(res => ({ ...res, data: res.data.map(n => MisskeyAPI.Converter.note(n, this.baseUrlToHost(this.baseUrl))) })) + .then(res => ({ ...res, data: res.data.map(n => this.converter.note(n, this.baseUrlToHost(this.baseUrl))) })) } /** @@ -1619,7 +1625,7 @@ export default class Misskey implements MegalodonInterface { } return this.client .post>('/api/notes/local-timeline', params) - .then(res => ({ ...res, data: res.data.map(n => MisskeyAPI.Converter.note(n, this.baseUrlToHost(this.baseUrl))) })) + .then(res => ({ ...res, data: res.data.map(n => this.converter.note(n, this.baseUrlToHost(this.baseUrl))) })) } /** @@ -1668,7 +1674,7 @@ export default class Misskey implements MegalodonInterface { } return this.client .post>('/api/notes/search-by-tag', params) - .then(res => ({ ...res, data: res.data.map(n => MisskeyAPI.Converter.note(n, this.baseUrlToHost(this.baseUrl))) })) + .then(res => ({ ...res, data: res.data.map(n => this.converter.note(n, this.baseUrlToHost(this.baseUrl))) })) } /** @@ -1708,7 +1714,7 @@ export default class Misskey implements MegalodonInterface { } return this.client .post>('/api/notes/timeline', params) - .then(res => ({ ...res, data: res.data.map(n => MisskeyAPI.Converter.note(n, this.baseUrlToHost(this.baseUrl))) })) + .then(res => ({ ...res, data: res.data.map(n => this.converter.note(n, this.baseUrlToHost(this.baseUrl))) })) } /** @@ -1751,7 +1757,7 @@ export default class Misskey implements MegalodonInterface { } return this.client .post>('/api/notes/user-list-timeline', params) - .then(res => ({ ...res, data: res.data.map(n => MisskeyAPI.Converter.note(n, this.baseUrlToHost(this.baseUrl))) })) + .then(res => ({ ...res, data: res.data.map(n => this.converter.note(n, this.baseUrlToHost(this.baseUrl))) })) } // ====================================== @@ -1793,7 +1799,7 @@ export default class Misskey implements MegalodonInterface { } return this.client .post>('/api/notes/mentions', params) - .then(res => ({ ...res, data: res.data.map(n => MisskeyAPI.Converter.noteToConversation(n, this.baseUrlToHost(this.baseUrl))) })) + .then(res => ({ ...res, data: res.data.map(n => this.converter.noteToConversation(n, this.baseUrlToHost(this.baseUrl))) })) } public async deleteConversation(_id: string): Promise> { @@ -1819,7 +1825,7 @@ export default class Misskey implements MegalodonInterface { public async getLists(): Promise>> { return this.client .post>('/api/users/lists/list') - .then(res => ({ ...res, data: res.data.map(l => MisskeyAPI.Converter.list(l)) })) + .then(res => ({ ...res, data: res.data.map(l => this.converter.list(l)) })) } /** @@ -1830,7 +1836,7 @@ export default class Misskey implements MegalodonInterface { .post('/api/users/lists/show', { listId: id }) - .then(res => ({ ...res, data: MisskeyAPI.Converter.list(res.data) })) + .then(res => ({ ...res, data: this.converter.list(res.data) })) } /** @@ -1841,7 +1847,7 @@ export default class Misskey implements MegalodonInterface { .post('/api/users/lists/create', { name: title }) - .then(res => ({ ...res, data: MisskeyAPI.Converter.list(res.data) })) + .then(res => ({ ...res, data: this.converter.list(res.data) })) } /** @@ -1853,7 +1859,7 @@ export default class Misskey implements MegalodonInterface { listId: id, name: title }) - .then(res => ({ ...res, data: MisskeyAPI.Converter.list(res.data) })) + .then(res => ({ ...res, data: this.converter.list(res.data) })) } /** @@ -1962,13 +1968,13 @@ export default class Misskey implements MegalodonInterface { } if (options.exclude_type) { params = Object.assign(params, { - excludeType: options.exclude_type.map(e => MisskeyAPI.Converter.encodeNotificationType(e)) + excludeType: options.exclude_type.map(e => this.converter.encodeNotificationType(e)) }) } } return this.client .post>('/api/i/notifications', params) - .then(res => ({ ...res, data: res.data.map(n => MisskeyAPI.Converter.notification(n, this.baseUrlToHost(this.baseUrl))) })) + .then(res => ({ ...res, data: res.data.map(n => this.converter.notification(n, this.baseUrlToHost(this.baseUrl))) })) } public async getNotification(_id: string): Promise> { @@ -2083,7 +2089,7 @@ export default class Misskey implements MegalodonInterface { return this.client.post>('/api/users/search', params).then(res => ({ ...res, data: { - accounts: res.data.map(u => MisskeyAPI.Converter.userDetail(u, this.baseUrlToHost(this.baseUrl))), + accounts: res.data.map(u => this.converter.userDetail(u, this.baseUrlToHost(this.baseUrl))), statuses: [], hashtags: [] } @@ -2124,7 +2130,7 @@ export default class Misskey implements MegalodonInterface { ...res, data: { accounts: [], - statuses: res.data.map(n => MisskeyAPI.Converter.note(n, this.baseUrlToHost(this.baseUrl))), + statuses: res.data.map(n => this.converter.note(n, this.baseUrlToHost(this.baseUrl))), hashtags: [] } })) @@ -2168,7 +2174,7 @@ export default class Misskey implements MegalodonInterface { const meta = await this.client.post('/api/meta').then(res => res.data) return this.client .post('/api/stats') - .then(res => ({ ...res, data: MisskeyAPI.Converter.meta(meta, res.data) })) + .then(res => ({ ...res, data: this.converter.meta(meta, res.data) })) } public async getInstancePeers(): Promise>> { @@ -2194,7 +2200,7 @@ export default class Misskey implements MegalodonInterface { public async getInstanceTrends(_limit?: number | null): Promise>> { return this.client .post>('/api/hashtags/trend') - .then(res => ({ ...res, data: res.data.map(h => MisskeyAPI.Converter.hashtag(h)) })) + .then(res => ({ ...res, data: res.data.map(h => this.converter.hashtag(h)) })) } // ====================================== @@ -2221,7 +2227,7 @@ export default class Misskey implements MegalodonInterface { public async getInstanceCustomEmojis(): Promise>> { return this.client .post('/api/meta') - .then(res => ({ ...res, data: res.data.emojis.map(e => MisskeyAPI.Converter.emoji(e)) })) + .then(res => ({ ...res, data: res.data.emojis.map(e => this.converter.emoji(e)) })) } // ====================================== @@ -2236,7 +2242,7 @@ export default class Misskey implements MegalodonInterface { } return this.client.post>('/api/announcements', params).then(res => ({ ...res, - data: res.data.map(t => MisskeyAPI.Converter.announcement(t)) + data: res.data.map(t => this.converter.announcement(t)) })) } @@ -2262,7 +2268,7 @@ export default class Misskey implements MegalodonInterface { .post('/api/notes/show', { noteId: id }) - .then(res => ({ ...res, data: MisskeyAPI.Converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) + .then(res => ({ ...res, data: this.converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) } /** @@ -2276,7 +2282,7 @@ export default class Misskey implements MegalodonInterface { .post('/api/notes/show', { noteId: id }) - .then(res => ({ ...res, data: MisskeyAPI.Converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) + .then(res => ({ ...res, data: this.converter.note(res.data, this.baseUrlToHost(this.baseUrl)) })) } public async getEmojiReactions(id: string): Promise>> { @@ -2286,7 +2292,7 @@ export default class Misskey implements MegalodonInterface { }) .then(res => ({ ...res, - data: MisskeyAPI.Converter.reactions(res.data) + data: this.converter.reactions(res.data) })) } diff --git a/megalodon/src/misskey/api_client.ts b/megalodon/src/misskey/api_client.ts index 7cbd747..b3447fb 100644 --- a/megalodon/src/misskey/api_client.ts +++ b/megalodon/src/misskey/api_client.ts @@ -44,9 +44,51 @@ namespace MisskeyAPI { export type APIEmoji = { emojis: Emoji[] } } - export namespace Converter { + 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 + } + + 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; + } + + + // FIXME: Properly render MFM instead of just escaping HTML characters. - const escapeMFM = (text: string): string => text + escapeMFM = (text: string): string => text .replace(/&/g, '&') .replace(//g, '>') @@ -55,7 +97,7 @@ namespace MisskeyAPI { .replace(/`/g, '`') .replace(/\r?\n/g, '
'); - export const emoji = (e: Entity.Emoji): MegalodonEntity.Emoji => { + emoji = (e: Entity.Emoji): MegalodonEntity.Emoji => { return { shortcode: e.name, static_url: e.url, @@ -65,15 +107,15 @@ namespace MisskeyAPI { } } - export const field = (f: Entity.Field): MegalodonEntity.Field => ({ + field = (f: Entity.Field): MegalodonEntity.Field => ({ name: f.name, - value: escapeMFM(f.value), + value: this.escapeMFM(f.value), verified_at: null }) - export const user = (u: Entity.User): MegalodonEntity.Account => { + user = (u: Entity.User): MegalodonEntity.Account => { let acct = u.username - let acctUrl = `https://${u.host || 'example.com'}/@${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}` @@ -91,20 +133,20 @@ namespace MisskeyAPI { note: '', url: acctUrl, avatar: u.avatarUrl, - avatar_static: u.avatarColor, - header: 'https://http.cat/404', // FIXME - header_static: 'https://http.cat/404', // FIXME - emojis: u.emojis.map(e => emoji(e)), + avatar_static: u.avatarUrl, + header: this.plcUrl, // FIXME + header_static: this.plcUrl, // FIXME + emojis: u.emojis.map(e => this.emoji(e)), moved: null, fields: [], bot: false } } - export const userDetail = (u: Entity.UserDetail, host: string): MegalodonEntity.Account => { + userDetail = (u: Entity.UserDetail, host: string): MegalodonEntity.Account => { let acct = u.username host = host.replace('https://', '') - let acctUrl = `https://${host || u.host || 'example.com'}/@${u.username}` + let acctUrl = `https://${host || u.host || this.instanceHost}/@${u.username}` if (u.host) { acct = `${u.username}@${u.host}` acctUrl = `https://${u.host}/@${u.username}` @@ -122,27 +164,27 @@ namespace MisskeyAPI { note: u.description, url: acct, avatar: u.avatarUrl, - avatar_static: u.avatarColor, - header: u.bannerUrl, - header_static: u.bannerColor, - emojis: u.emojis.map(e => emoji(e)), + avatar_static: u.avatarUrl, + header: u.bannerUrl ?? this.plcUrl, + header_static: u.bannerUrl ?? this.plcUrl, + emojis: u.emojis.map(e => this.emoji(e)), moved: null, - fields: u.fields.map(f => field(f)), + fields: u.fields.map(f => this.field(f)), bot: u.isBot, } } - export const userPreferences = (u: MisskeyAPI.Entity.UserDetailMe, g: MisskeyAPI.Entity.GetAll): MegalodonEntity.Preferences => { + userPreferences = (u: MisskeyAPI.Entity.UserDetailMe, g: MisskeyAPI.Entity.GetAll): MegalodonEntity.Preferences => { return { "reading:expand:media": "default", "reading:expand:spoilers": false, "posting:default:language": u.lang, "posting:default:sensitive": u.alwaysMarkNsfw, - "posting:default:visibility": visibility(g.defaultNoteVisibility) + "posting:default:visibility": this.visibility(g.defaultNoteVisibility) } } - export const visibility = (v: 'public' | 'home' | 'followers' | 'specified'): 'public' | 'unlisted' | 'private' | 'direct' => { + visibility = (v: 'public' | 'home' | 'followers' | 'specified'): 'public' | 'unlisted' | 'private' | 'direct' => { switch (v) { case 'public': return v @@ -155,7 +197,7 @@ namespace MisskeyAPI { } } - export const encodeVisibility = (v: 'public' | 'unlisted' | 'private' | 'direct'): 'public' | 'home' | 'followers' | 'specified' => { + encodeVisibility = (v: 'public' | 'unlisted' | 'private' | 'direct'): 'public' | 'home' | 'followers' | 'specified' => { switch (v) { case 'public': return v @@ -168,7 +210,7 @@ namespace MisskeyAPI { } } - export const fileType = (s: string): 'unknown' | 'image' | 'gifv' | 'video' | 'audio' => { + fileType = (s: string): 'unknown' | 'image' | 'gifv' | 'video' | 'audio' => { if (s === 'image/gif') { return 'gifv' } @@ -184,10 +226,10 @@ namespace MisskeyAPI { return 'unknown' } - export const file = (f: Entity.File): MegalodonEntity.Attachment => { + file = (f: Entity.File): MegalodonEntity.Attachment => { return { id: f.id, - type: fileType(f.type), + type: this.fileType(f.type), url: f.url, remote_url: f.url, preview_url: f.thumbnailUrl, @@ -201,15 +243,15 @@ namespace MisskeyAPI { } } - export const follower = (f: Entity.Follower): MegalodonEntity.Account => { - return user(f.follower) + follower = (f: Entity.Follower): MegalodonEntity.Account => { + return this.user(f.follower) } - export const following = (f: Entity.Following): MegalodonEntity.Account => { - return user(f.followee) + following = (f: Entity.Following): MegalodonEntity.Account => { + return this.user(f.followee) } - export const relation = (r: Entity.Relation): MegalodonEntity.Relationship => { + relation = (r: Entity.Relation): MegalodonEntity.Relationship => { return { id: r.id, following: r.isFollowing, @@ -226,14 +268,14 @@ namespace MisskeyAPI { } } - export const choice = (c: Entity.Choice): MegalodonEntity.PollOption => { + choice = (c: Entity.Choice): MegalodonEntity.PollOption => { return { title: c.text, votes_count: c.votes } } - export const poll = (p: Entity.Poll): MegalodonEntity.Poll => { + 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) @@ -243,49 +285,49 @@ namespace MisskeyAPI { expired: now.isAfter(expire), multiple: p.multiple, votes_count: count, - options: p.choices.map(c => choice(c)), + options: p.choices.map(c => this.choice(c)), voted: p.choices.some(c => c.isVoted) } } - export const note = (n: Entity.Note, host: string): MegalodonEntity.Status => { + note = (n: Entity.Note, host: string): MegalodonEntity.Status => { host = host.replace('https://', '') 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: user(n.user), + account: this.user(n.user), in_reply_to_id: n.replyId, in_reply_to_account_id: n.reply?.userId ?? null, - reblog: n.renote ? note(n.renote, host) : null, - content: n.text ? escapeMFM(n.text) : '', + 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, - emojis: n.emojis.map(e => emoji(e)), + emojis: n.emojis.map(e => this.emoji(e)), replies_count: n.repliesCount, reblogs_count: n.renoteCount, - favourites_count: getTotalReactions(n.reactions), // FIXME: instead get # of default reaction emoji reactions + favourites_count: this.getTotalReactions(n.reactions), // FIXME: instead get # of default reaction emoji 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: visibility(n.visibility), - media_attachments: n.files ? n.files.map(f => file(f)) : [], + visibility: this.visibility(n.visibility), + media_attachments: n.files ? n.files.map(f => this.file(f)) : [], mentions: [], tags: [], card: null, - poll: n.poll ? poll(n.poll) : null, + poll: n.poll ? this.poll(n.poll) : null, application: null, language: null, pinned: null, - emoji_reactions: mapReactions(n.reactions, n.myReaction), + emoji_reactions: this.mapReactions(n.reactions, n.myReaction), bookmarked: false, - quote: n.renote && n.text ? note(n.renote, host) : null + quote: n.renote && n.text ? this.note(n.renote, host) : null } } - export const mapReactions = (r: { [key: string]: number }, myReaction?: string): Array => { + mapReactions = (r: { [key: string]: number }, myReaction?: string): Array => { return Object.keys(r).map(key => { if (myReaction && key === myReaction) { return { @@ -302,11 +344,11 @@ namespace MisskeyAPI { }) } - export const getTotalReactions = (r: { [key: string]: number }): number => { + getTotalReactions = (r: { [key: string]: number }): number => { return Object.values(r).length > 0 ? Object.values(r).reduce((previousValue, currentValue) => previousValue + currentValue) : 0 } - export const reactions = (r: Array): Array => { + reactions = (r: Array): Array => { const result: Array = [] for (const e of r) { const i = result.findIndex(res => res.name === e.type) @@ -323,25 +365,25 @@ namespace MisskeyAPI { return result } - export const noteToConversation = (n: Entity.Note, host: string): MegalodonEntity.Conversation => { - const accounts: Array = [user(n.user)] + noteToConversation = (n: Entity.Note, host: string): MegalodonEntity.Conversation => { + const accounts: Array = [this.user(n.user)] if (n.reply) { - accounts.push(user(n.reply.user)) + accounts.push(this.user(n.reply.user)) } return { id: n.id, accounts: accounts, - last_status: note(n, host), + last_status: this.note(n, host), unread: false } } - export const list = (l: Entity.List): MegalodonEntity.List => ({ + list = (l: Entity.List): MegalodonEntity.List => ({ id: l.id, title: l.name }) - export const encodeNotificationType = (e: MegalodonEntity.NotificationType): MisskeyEntity.NotificationType => { + encodeNotificationType = (e: MegalodonEntity.NotificationType): MisskeyEntity.NotificationType => { switch (e) { case NotificationType.Follow: return MisskeyNotificationType.Follow @@ -361,7 +403,7 @@ namespace MisskeyAPI { } } - export const decodeNotificationType = (e: MisskeyEntity.NotificationType): MegalodonEntity.NotificationType => { + decodeNotificationType = (e: MisskeyEntity.NotificationType): MegalodonEntity.NotificationType => { switch (e) { case MisskeyNotificationType.Follow: return NotificationType.Follow @@ -383,35 +425,12 @@ namespace MisskeyAPI { 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, - last_status_at: '1971-01-01T00:00:00.000Z', - noindex: true, - emojis: [], - fields: [], - moved: null - } - export const announcement = (a: Entity.Announcement): MegalodonEntity.Announcement => ({ + + + announcement = (a: Entity.Announcement): MegalodonEntity.Announcement => ({ id: a.id, - content: `

${escapeMFM(a.title)}

${escapeMFM(a.text)}`, + content: `

${this.escapeMFM(a.title)}

${this.escapeMFM(a.text)}`, starts_at: null, ends_at: null, published: true, @@ -426,16 +445,16 @@ namespace MisskeyAPI { reactions: [], }) - export const notification = (n: Entity.Notification, host: string): MegalodonEntity.Notification => { + notification = (n: Entity.Notification, host: string): MegalodonEntity.Notification => { let notification = { id: n.id, - account: n.user ? user(n.user) : modelOfAcct, + account: n.user ? this.user(n.user) : this.modelOfAcct, created_at: n.createdAt, - type: decodeNotificationType(n.type) + type: this.decodeNotificationType(n.type) } if (n.note) { notification = Object.assign(notification, { - status: note(n.note, host) + status: this.note(n.note, host) }) } if (n.reaction) { @@ -446,7 +465,7 @@ namespace MisskeyAPI { return notification } - export const stats = (s: Entity.Stats): MegalodonEntity.Stats => { + stats = (s: Entity.Stats): MegalodonEntity.Stats => { return { user_count: s.usersCount, status_count: s.notesCount, @@ -454,7 +473,7 @@ namespace MisskeyAPI { } } - export const meta = (m: Entity.Meta, s: Entity.Stats): MegalodonEntity.Instance => { + meta = (m: Entity.Meta, s: Entity.Stats): MegalodonEntity.Instance => { const wss = m.uri.replace(/^https:\/\//, 'wss://') return { uri: m.uri, @@ -466,7 +485,7 @@ namespace MisskeyAPI { urls: { streaming_api: `${wss}/streaming` }, - stats: stats(s), + stats: this.stats(s), languages: m.langs, contact_account: null, max_toot_chars: m.maxNoteTextLength, @@ -474,7 +493,7 @@ namespace MisskeyAPI { } } - export const hashtag = (h: Entity.Hashtag): MegalodonEntity.Tag => { + hashtag = (h: Entity.Hashtag): MegalodonEntity.Tag => { return { name: h.tag, url: h.tag, @@ -525,19 +544,22 @@ namespace MisskeyAPI { 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) { + 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 } @@ -570,6 +592,7 @@ namespace MisskeyAPI { } } + console.log(`sending request to ${this.baseUrl}${path} with params:`); console.log(JSON.stringify(bodyParams, null, 2)); return axios.post(this.baseUrl + path, bodyParams, options).then((resp: AxiosResponse) => { @@ -605,7 +628,7 @@ namespace MisskeyAPI { throw new Error('accessToken is required') } const url = `${this.baseUrl}/streaming` - const streaming = new WebSocket(url, channel, this.accessToken, listId, this.userAgent, this.proxyConfig) + const streaming = new WebSocket(url, channel, this.accessToken, listId, this.userAgent, this.proxyConfig, this.converter) process.nextTick(() => { streaming.start() }) diff --git a/megalodon/src/misskey/web_socket.ts b/megalodon/src/misskey/web_socket.ts index 0529068..d364286 100644 --- a/megalodon/src/misskey/web_socket.ts +++ b/megalodon/src/misskey/web_socket.ts @@ -18,6 +18,7 @@ export default class WebSocket extends EventEmitter implements WebSocketInterfac public headers: { [key: string]: string } public proxyConfig: ProxyConfig | false = false public listId: string | null = null + private _converter: MisskeyAPI.Converter private _accessToken: string private _reconnectInterval: number private _reconnectMaxAttempts: number @@ -41,7 +42,8 @@ export default class WebSocket extends EventEmitter implements WebSocketInterfac accessToken: string, listId: string | undefined, userAgent: string, - proxyConfig: ProxyConfig | false = false + proxyConfig: ProxyConfig | false = false, + converter: MisskeyAPI.Converter ) { super() this.url = url @@ -63,6 +65,7 @@ export default class WebSocket extends EventEmitter implements WebSocketInterfac this._connectionClosed = false this._channelID = uuid() this._pongReceivedTimestamp = dayjs() + this._converter = converter } /** @@ -286,13 +289,13 @@ export default class WebSocket extends EventEmitter implements WebSocketInterfac */ private _setupParser() { this.parser.on('update', (note: MisskeyAPI.Entity.Note) => { - this.emit('update', MisskeyAPI.Converter.note(note, this.baseUrlToHost(this.url))) + this.emit('update', this._converter.note(note, this.baseUrlToHost(this.url))) }) this.parser.on('notification', (notification: MisskeyAPI.Entity.Notification) => { - this.emit('notification', MisskeyAPI.Converter.notification(notification, this.baseUrlToHost(this.url))) + this.emit('notification', this._converter.notification(notification, this.baseUrlToHost(this.url))) }) this.parser.on('conversation', (note: MisskeyAPI.Entity.Note) => { - this.emit('conversation', MisskeyAPI.Converter.noteToConversation(note, this.baseUrlToHost(this.url))) + this.emit('conversation', this._converter.noteToConversation(note, this.baseUrlToHost(this.url))) }) this.parser.on('error', (err: Error) => { this.emit('parser-error', err) diff --git a/megalodon/test/unit/misskey/api_client.spec.ts b/megalodon/test/unit/misskey/api_client.spec.ts index ceb0211..acaac39 100644 --- a/megalodon/test/unit/misskey/api_client.spec.ts +++ b/megalodon/test/unit/misskey/api_client.spec.ts @@ -14,6 +14,8 @@ const user: MisskeyEntity.User = { emojis: [] } +const converter: MisskeyAPI.Converter = new MisskeyAPI.Converter("https://example.com") + describe('api_client', () => { describe('notification', () => { describe('encode', () => { @@ -49,7 +51,7 @@ describe('api_client', () => { } ] cases.forEach(c => { - expect(MisskeyAPI.Converter.encodeNotificationType(c.src)).toEqual(c.dist) + expect(converter.encodeNotificationType(c.src)).toEqual(c.dist) }) }) }) @@ -94,7 +96,7 @@ describe('api_client', () => { } ] cases.forEach(c => { - expect(MisskeyAPI.Converter.decodeNotificationType(c.src)).toEqual(c.dist) + expect(converter.decodeNotificationType(c.src)).toEqual(c.dist) }) }) }) @@ -160,7 +162,7 @@ describe('api_client', () => { } ] - const reactions = MisskeyAPI.Converter.reactions(misskeyReactions) + const reactions = converter.reactions(misskeyReactions) expect(reactions).toEqual([ { count: 3, @@ -198,7 +200,7 @@ describe('api_client', () => { replyId: null, renoteId: null } - const megalodonStatus = MisskeyAPI.Converter.note(note, user.host || 'misskey.io') + const megalodonStatus = converter.note(note, user.host || 'misskey.io') expect(megalodonStatus.plain_content).toEqual(plainContent) expect(megalodonStatus.content).toEqual(content) }) @@ -222,7 +224,7 @@ describe('api_client', () => { replyId: null, renoteId: null } - const megalodonStatus = MisskeyAPI.Converter.note(note, user.host || 'misskey.io') + const megalodonStatus = converter.note(note, user.host || 'misskey.io') expect(megalodonStatus.plain_content).toEqual(plainContent) expect(megalodonStatus.content).toEqual(content) })