From 343a507eccc16b27cf2103fd4222d521fb18f092 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 28 May 2025 17:17:03 +0200 Subject: [PATCH] feat(federation): :sparkles: Federate Reactions --- classes/database/reaction.ts | 14 +++++ classes/database/user.ts | 72 +++++++++++++++++++++++-- packages/client/schemas/notification.ts | 1 + types/api.ts | 3 +- 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/classes/database/reaction.ts b/classes/database/reaction.ts index a64d372d..e08d73c6 100644 --- a/classes/database/reaction.ts +++ b/classes/database/reaction.ts @@ -236,6 +236,20 @@ export class Reaction extends BaseInterface { }); } + public toVersiaUnreact(): VersiaEntities.Delete { + return new VersiaEntities.Delete({ + type: "Delete", + id: crypto.randomUUID(), + created_at: new Date().toISOString(), + author: User.getUri( + this.data.authorId, + this.data.author.uri ? new URL(this.data.author.uri) : null, + ).href, + deleted_type: "pub.versia:reactions/Reaction", + deleted: this.getUri(config.http.base_url).href, + }); + } + public static async fromVersia( reactionToConvert: VersiaEntities.Reaction, author: User, diff --git a/classes/database/user.ts b/classes/database/user.ts index 1ab6615d..3486b548 100644 --- a/classes/database/user.ts +++ b/classes/database/user.ts @@ -752,7 +752,7 @@ export class User extends BaseInterface { } // Create the reaction - await Reaction.insert({ + const reaction = await Reaction.insert({ id: randomUUIDv7(), authorId: this.id, noteId: note.id, @@ -760,7 +760,29 @@ export class User extends BaseInterface { emojiId: emoji instanceof Emoji ? emoji.id : null, }); - // TODO: Handle federation and notifications + const finalNote = await Note.fromId(note.id, this.id); + + if (!finalNote) { + throw new Error("Failed to fetch note after reaction"); + } + + if (note.author.local) { + // Notify the user that their post has been reacted to + await note.author.notify("reaction", this, finalNote); + } + + if (this.local) { + const federatedUsers = await this.federateToFollowers( + reaction.toVersia(), + ); + + if ( + note.remote && + !federatedUsers.find((u) => u.id === note.author.id) + ) { + await this.federateToUser(reaction.toVersia(), note.author); + } + } } /** @@ -777,11 +799,45 @@ export class User extends BaseInterface { await reactionToDelete.delete(); - // TODO: Handle federation and notifications + if (note.author.local) { + // Remove any eventual notifications for this reaction + await db + .delete(Notifications) + .where( + and( + eq(Notifications.accountId, this.id), + eq(Notifications.type, "reaction"), + eq(Notifications.notifiedId, note.data.authorId), + eq(Notifications.noteId, note.id), + ), + ); + } + + if (this.local) { + const federatedUsers = await this.federateToFollowers( + reactionToDelete.toVersiaUnreact(), + ); + + if ( + note.remote && + !federatedUsers.find((u) => u.id === note.author.id) + ) { + await this.federateToUser( + reactionToDelete.toVersiaUnreact(), + note.author, + ); + } + } } public async notify( - type: "mention" | "follow_request" | "follow" | "favourite" | "reblog", + type: + | "mention" + | "follow_request" + | "follow" + | "favourite" + | "reblog" + | "reaction", relatedUser: User, note?: Note, ): Promise { @@ -801,7 +857,13 @@ export class User extends BaseInterface { private async notifyPush( notificationId: string, - type: "mention" | "follow_request" | "follow" | "favourite" | "reblog", + type: + | "mention" + | "follow_request" + | "follow" + | "favourite" + | "reblog" + | "reaction", relatedUser: User, note?: Note, ): Promise { diff --git a/packages/client/schemas/notification.ts b/packages/client/schemas/notification.ts index 57541b46..32f9318c 100644 --- a/packages/client/schemas/notification.ts +++ b/packages/client/schemas/notification.ts @@ -21,6 +21,7 @@ export const Notification = z "favourite", "poll", "update", + "reaction", "admin.sign_up", "admin.report", "severed_relationships", diff --git a/types/api.ts b/types/api.ts index 6ca3f549..3e976c69 100644 --- a/types/api.ts +++ b/types/api.ts @@ -32,4 +32,5 @@ export type KnownEntity = | VersiaEntities.Unfollow | VersiaEntities.Delete | VersiaEntities.Like - | VersiaEntities.Share; + | VersiaEntities.Share + | VersiaEntities.Reaction;