From 3c5468086047a29cc56c90ab6547484364c22326 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 22 Mar 2025 18:18:54 -0400 Subject: [PATCH] support reactions in mastodon API --- .../src/server/api/mastodon/converters.ts | 62 ++++++++++--------- .../api/mastodon/endpoints/notifications.ts | 23 +++---- packages/megalodon/src/entities/reaction.ts | 2 + .../src/mastodon/entities/reaction.ts | 16 +++++ .../megalodon/src/mastodon/entities/status.ts | 3 + packages/megalodon/src/mastodon/entity.ts | 1 + packages/megalodon/src/misskey.ts | 1 + packages/megalodon/src/misskey/api_client.ts | 28 ++++----- 8 files changed, 82 insertions(+), 54 deletions(-) create mode 100644 packages/megalodon/src/mastodon/entities/reaction.ts diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index 0e468f9377..1adbd95642 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -180,10 +180,10 @@ export class MastoConverters { note: profile?.description ?? '', url: user.uri ?? acctUrl, uri: user.uri ?? acctUri, - avatar: user.avatarUrl ? user.avatarUrl : 'https://dev.joinsharkey.org/static-assets/avatar.png', - avatar_static: user.avatarUrl ? user.avatarUrl : 'https://dev.joinsharkey.org/static-assets/avatar.png', - header: user.bannerUrl ? user.bannerUrl : 'https://dev.joinsharkey.org/static-assets/transparent.png', - header_static: user.bannerUrl ? user.bannerUrl : 'https://dev.joinsharkey.org/static-assets/transparent.png', + avatar: user.avatarUrl ?? 'https://dev.joinsharkey.org/static-assets/avatar.png', + avatar_static: user.avatarUrl ?? 'https://dev.joinsharkey.org/static-assets/avatar.png', + header: user.bannerUrl ?? 'https://dev.joinsharkey.org/static-assets/transparent.png', + header_static: user.bannerUrl ?? 'https://dev.joinsharkey.org/static-assets/transparent.png', emojis: emoji, moved: null, //FIXME fields: profile?.fields.map(p => this.encodeField(p)) ?? [], @@ -196,7 +196,7 @@ export class MastoConverters { }); } - public async getEdits(id: string, me?: MiLocalUser | null): Promise { + public async getEdits(id: string, me: MiLocalUser | null): Promise { const note = await this.mastodonDataService.getNote(id, me); if (!note) { return []; @@ -213,7 +213,7 @@ export class MastoConverters { account: noteUser, content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)) ?? '', created_at: lastDate.toISOString(), - emojis: [], + emojis: [], //FIXME sensitive: edit.cw != null && edit.cw.length > 0, spoiler_text: edit.cw ?? '', media_attachments: files.length > 0 ? files.map((f) => this.encodeFile(f)) : [], @@ -222,15 +222,15 @@ export class MastoConverters { history.push(item); } - return await Promise.all(history); + return history; } - private async convertReblog(status: Entity.Status | null, me?: MiLocalUser | null): Promise { + private async convertReblog(status: Entity.Status | null, me: MiLocalUser | null): Promise { if (!status) return null; return await this.convertStatus(status, me); } - public async convertStatus(status: Entity.Status, me?: MiLocalUser | null): Promise { + public async convertStatus(status: Entity.Status, me: MiLocalUser | null): Promise { const convertedAccount = this.convertAccount(status.account); const note = await this.mastodonDataService.requireNote(status.id, me); const noteUser = await this.getUser(status.account.id); @@ -279,7 +279,6 @@ export class MastoConverters { : ''; const reblogged = await this.mastodonDataService.hasReblog(note.id, me); - const reactions = await Promise.all(status.emoji_reactions.map(r => this.convertReaction(r))); // noinspection ES6MissingAwait return await awaitAll({ @@ -289,11 +288,12 @@ export class MastoConverters { account: convertedAccount, in_reply_to_id: note.replyId, in_reply_to_account_id: note.replyUserId, - reblog: !isQuote ? await this.convertReblog(status.reblog, me) : null, + reblog: !isQuote ? this.convertReblog(status.reblog, me) : null, content: content, content_type: 'text/x.misskeymarkdown', text: note.text, created_at: status.created_at, + edited_at: note.updatedAt?.toISOString() ?? null, emojis: emoji, replies_count: note.repliesCount, reblogs_count: note.renoteCount, @@ -301,7 +301,7 @@ export class MastoConverters { reblogged, favourited: status.favourited, muted: status.muted, - sensitive: status.sensitive, + sensitive: status.sensitive || !!note.cw, spoiler_text: note.cw ?? '', visibility: status.visibility, media_attachments: status.media_attachments.map(a => convertAttachment(a)), @@ -312,15 +312,14 @@ export class MastoConverters { application: null, //FIXME language: null, //FIXME pinned: false, //FIXME - reactions, - emoji_reactions: reactions, bookmarked: false, //FIXME - quote: isQuote ? await this.convertReblog(status.reblog, me) : null, - edited_at: note.updatedAt?.toISOString() ?? null, + quote_id: isQuote ? status.reblog?.id : undefined, + quote: isQuote ? this.convertReblog(status.reblog, me) : null, + reactions: status.emoji_reactions, }); } - public async convertConversation(conversation: Entity.Conversation, me?: MiLocalUser | null): Promise { + public async convertConversation(conversation: Entity.Conversation, me: MiLocalUser | null): Promise { return { id: conversation.id, accounts: await Promise.all(conversation.accounts.map(a => this.convertAccount(a))), @@ -329,7 +328,7 @@ export class MastoConverters { }; } - public async convertNotification(notification: Entity.Notification, me?: MiLocalUser | null): Promise { + public async convertNotification(notification: Entity.Notification, me: MiLocalUser | null): Promise { return { account: await this.convertAccount(notification.account), created_at: notification.created_at, @@ -339,12 +338,23 @@ export class MastoConverters { }; } - public async convertReaction(reaction: Entity.Reaction): Promise { - if (reaction.accounts) { - reaction.accounts = await Promise.all(reaction.accounts.map(a => this.convertAccount(a))); - } - return reaction; - } + // public convertEmoji(emoji: string): MastodonEntity.Emoji { + // const reaction: MastodonEntity.Reaction = { + // name: emoji, + // count: 1, + // }; + // + // if (emoji.startsWith(':')) { + // const [, name] = emoji.match(/^:([^@:]+(?:@[^@:]+)?):$/) ?? []; + // if (name) { + // const url = `${this.config.url}/emoji/${name}.webp`; + // reaction.url = url; + // reaction.static_url = url; + // } + // } + // + // return reaction; + // } } function simpleConvert(data: T): T { @@ -423,7 +433,3 @@ export function convertRelationship(relationship: Partial & }; } -// noinspection JSUnusedGlobalSymbols -export function convertStatusSource(status: Entity.StatusSource): MastodonEntity.StatusSource { - return simpleConvert(status); -} diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts index 6acb9edd6b..120b9ba7f9 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts @@ -29,13 +29,17 @@ export class ApiNotificationsMastodon { fastify.get('/v1/notifications', async (request, reply) => { const { client, me } = await this.clientService.getAuthClient(request); const data = await client.getNotifications(parseTimelineArgs(request.query)); - const response = await Promise.all(data.data.map(async n => { - const converted = await this.mastoConverters.convertNotification(n, me); - if (converted.type === 'reaction') { - converted.type = 'favourite'; + const notifications = await Promise.all(data.data.map(n => this.mastoConverters.convertNotification(n, me))); + const response: MastodonEntity.Notification[] = []; + for (const notification of notifications) { + response.push(notification); + if (notification.type === 'reaction') { + response.push({ + ...notification, + type: 'favourite', + }); } - return converted; - })); + } attachMinMaxPagination(request, reply, response); reply.send(response); @@ -46,12 +50,9 @@ export class ApiNotificationsMastodon { const { client, me } = await this.clientService.getAuthClient(_request); const data = await client.getNotification(_request.params.id); - const converted = await this.mastoConverters.convertNotification(data.data, me); - if (converted.type === 'reaction') { - converted.type = 'favourite'; - } + const response = await this.mastoConverters.convertNotification(data.data, me); - reply.send(converted); + reply.send(response); }); fastify.post('/v1/notification/:id/dismiss', { preHandler: upload.single('none') }, async (_request, reply) => { diff --git a/packages/megalodon/src/entities/reaction.ts b/packages/megalodon/src/entities/reaction.ts index 8c626f9e84..3315eded50 100644 --- a/packages/megalodon/src/entities/reaction.ts +++ b/packages/megalodon/src/entities/reaction.ts @@ -6,5 +6,7 @@ namespace Entity { me: boolean name: string accounts?: Array + url?: string + static_url?: string } } diff --git a/packages/megalodon/src/mastodon/entities/reaction.ts b/packages/megalodon/src/mastodon/entities/reaction.ts new file mode 100644 index 0000000000..370eeb5cbe --- /dev/null +++ b/packages/megalodon/src/mastodon/entities/reaction.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/// + +namespace MastodonEntity { + export type Reaction = { + name: string + count: number + me?: boolean + url?: string + static_url?: string + } +} diff --git a/packages/megalodon/src/mastodon/entities/status.ts b/packages/megalodon/src/mastodon/entities/status.ts index 54b5d3bfe3..76472a8580 100644 --- a/packages/megalodon/src/mastodon/entities/status.ts +++ b/packages/megalodon/src/mastodon/entities/status.ts @@ -6,6 +6,7 @@ /// /// /// +/// namespace MastodonEntity { export type Status = { @@ -41,6 +42,8 @@ namespace MastodonEntity { // These parameters are unique parameters in fedibird.com for quote. quote_id?: string quote?: Status | null + // These parameters are unique to glitch-soc for emoji reactions. + reactions?: Reaction[] } export type StatusTag = { diff --git a/packages/megalodon/src/mastodon/entity.ts b/packages/megalodon/src/mastodon/entity.ts index dcafdfe749..10a3aa71c4 100644 --- a/packages/megalodon/src/mastodon/entity.ts +++ b/packages/megalodon/src/mastodon/entity.ts @@ -22,6 +22,7 @@ /// /// /// +/// /// /// /// diff --git a/packages/megalodon/src/misskey.ts b/packages/megalodon/src/misskey.ts index dce0fb21b7..eb1e5824b8 100644 --- a/packages/megalodon/src/misskey.ts +++ b/packages/megalodon/src/misskey.ts @@ -2555,6 +2555,7 @@ export default class Misskey implements MegalodonInterface { })) } + // TODO implement public async getEmojiReaction(_id: string, _emoji: string): Promise> { return new Promise((_, reject) => { const err = new NoImplementedError('misskey does not support') diff --git a/packages/megalodon/src/misskey/api_client.ts b/packages/megalodon/src/misskey/api_client.ts index a9a592b28c..ce6c4aa6cc 100644 --- a/packages/megalodon/src/misskey/api_client.ts +++ b/packages/megalodon/src/misskey/api_client.ts @@ -286,6 +286,7 @@ namespace MisskeyAPI { plain_content: n.text ? n.text : null, created_at: n.createdAt, edited_at: n.updatedAt || null, + // TODO this is probably wrong emojis: mapEmojis(n.emojis).concat(mapReactionEmojis(n.reactionEmojis)), replies_count: n.repliesCount, reblogs_count: n.renoteCount, @@ -304,7 +305,7 @@ namespace MisskeyAPI { application: null, language: null, pinned: null, - emoji_reactions: typeof n.reactions === 'object' ? mapReactions(n.reactions, n.myReaction) : [], + emoji_reactions: typeof n.reactions === 'object' ? mapReactions(n.reactions, n.reactionEmojis, n.myReaction) : [], bookmarked: false, quote: n.renote && n.text ? note(n.renote, n.user.host ? n.user.host : host ? host : null) : null } @@ -334,23 +335,20 @@ namespace MisskeyAPI { ) : 0; }; - export const mapReactions = (r: { [key: string]: number }, myReaction?: string): Array => { + export const mapReactions = (r: { [key: string]: number }, e: Record, myReaction?: string): Array => { 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 - } + const me = myReaction != null && key === myReaction; + return { + count: r[key], + me, + name: key, + url: e[key], + static_url: e[key], + } }) } + // TODO implement other properties const mapReactionEmojis = (r: { [key: string]: string }): Array => { return Object.keys(r).map(key => ({ shortcode: key, @@ -371,7 +369,7 @@ namespace MisskeyAPI { result.push({ count: 1, me: false, - name: e.type + name: e.type, }) } })