diff --git a/api/inbox/index.test.ts b/api/inbox/index.test.ts index 43c794ac..9a571bed 100644 --- a/api/inbox/index.test.ts +++ b/api/inbox/index.test.ts @@ -9,9 +9,10 @@ import { import { and, eq } from "drizzle-orm"; import { Instance } from "~/classes/database/instance"; import { Note } from "~/classes/database/note"; +import { Reaction } from "~/classes/database/reaction"; import { User } from "~/classes/database/user"; import { config } from "~/config"; -import { Notes } from "~/drizzle/schema"; +import { Notes, Reactions, Users } from "~/drizzle/schema"; import { sign } from "~/packages/sdk/crypto"; import * as VersiaEntities from "~/packages/sdk/entities"; import { fakeRequest } from "~/tests/utils"; @@ -20,6 +21,7 @@ const instanceUrl = new URL("https://versia.example.com"); const noteId = randomUUIDv7(); const userId = randomUUIDv7(); const shareId = randomUUIDv7(); +const reactionId = randomUUIDv7(); const userKeys = await User.generateKeys(); const privateKey = await crypto.subtle.importKey( "pkcs8", @@ -222,7 +224,71 @@ describe("Inbox Tests", () => { expect(share).not.toBeNull(); }); - test("should correctly process Delete for Note", async () => { + test("should correctly process Reaction", async () => { + const exampleRequest = new VersiaEntities.Reaction({ + id: reactionId, + created_at: "2025-04-18T10:32:01.427Z", + uri: new URL(`/reactions/${reactionId}`, instanceUrl).href, + type: "pub.versia:reactions/Reaction", + author: new URL(`/users/${userId}`, instanceUrl).href, + object: new URL(`/notes/${noteId}`, instanceUrl).href, + content: "👍", + }); + + const signedRequest = await sign( + privateKey, + new URL(exampleRequest.data.author), + new Request(inboxUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "User-Agent": "Versia/1.0.0", + }, + body: JSON.stringify(exampleRequest.toJSON()), + }), + ); + + const response = await fakeRequest(inboxUrl, { + method: "POST", + headers: signedRequest.headers, + body: signedRequest.body, + }); + + expect(response.status).toBe(200); + + await sleep(500); + + const dbNote = await Note.fromSql( + eq(Notes.uri, new URL(`/notes/${noteId}`, instanceUrl).href), + ); + + if (!dbNote) { + throw new Error("DBNote not found"); + } + + // Find the remote user who reacted by URI + const remoteUser = await User.fromSql( + eq(Users.uri, new URL(`/users/${userId}`, instanceUrl).href), + ); + + if (!remoteUser) { + throw new Error("Remote user not found"); + } + + // Check if reaction was created in the database + const reaction = await Reaction.fromSql( + and( + eq(Reactions.noteId, dbNote.id), + eq(Reactions.authorId, remoteUser.id), + eq(Reactions.emojiText, "👍"), + ), + ); + + expect(reaction).not.toBeNull(); + }); + + test("should correctly process Delete", async () => { const deleteId = randomUUIDv7(); // First check that the note exists in the database diff --git a/classes/inbox/processor.ts b/classes/inbox/processor.ts index ecdab5d3..b4497bc3 100644 --- a/classes/inbox/processor.ts +++ b/classes/inbox/processor.ts @@ -1,5 +1,12 @@ import { getLogger, type Logger } from "@logtape/logtape"; -import { type Instance, Like, Note, Relationship, User } from "@versia/kit/db"; +import { + type Instance, + Like, + Note, + Reaction, + Relationship, + User, +} from "@versia/kit/db"; import { Likes, Notes } from "@versia/kit/tables"; import type { SocketAddress } from "bun"; import { Glob } from "bun"; @@ -198,8 +205,9 @@ export class InboxProcessor { InboxProcessor.processDelete(d), ) .on(VersiaEntities.User, (u) => InboxProcessor.processUser(u)) - .on(VersiaEntities.Share, async (s) => - InboxProcessor.processShare(s), + .on(VersiaEntities.Share, (s) => InboxProcessor.processShare(s)) + .on(VersiaEntities.Reaction, (r) => + InboxProcessor.processReaction(r), ) .sort(() => { throw new ApiError(400, "Unknown entity type"); @@ -209,6 +217,29 @@ export class InboxProcessor { } } + /** + * Handles Reaction entity processing + * + * @param {VersiaEntities.Reaction} reaction - The Reaction entity to process. + * @returns {Promise} + */ + private static async processReaction( + reaction: VersiaEntities.Reaction, + ): Promise { + const author = await User.resolve(new URL(reaction.data.author)); + const note = await Note.resolve(new URL(reaction.data.object)); + + if (!author) { + throw new ApiError(404, "Author not found"); + } + + if (!note) { + throw new ApiError(404, "Note not found"); + } + + await Reaction.fromVersia(reaction, author, note); + } + /** * Handles Note entity processing *