From 47133ac3feef1ecbe368cf11ad059f584ee718dc Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 15 Apr 2024 19:40:35 -1000 Subject: [PATCH] feat(api): :sparkles: Add notifications for follow requests again and mentions --- database/entities/Status.ts | 25 ++++ database/entities/User.ts | 6 +- server/api/api/v1/notifications/index.test.ts | 110 ++++++++++++++++++ server/api/api/v1/notifications/index.ts | 2 - 4 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 server/api/api/v1/notifications/index.test.ts diff --git a/database/entities/Status.ts b/database/entities/Status.ts index 023e252a..da7fcea6 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -35,6 +35,7 @@ import { status, statusToMentions, user, + notification, } from "~drizzle/schema"; import { LogLevel } from "~packages/log-manager"; import type { Note } from "~types/lysand/Object"; @@ -995,6 +996,18 @@ export const createNewStatus = async ( .where(inArray(attachment.id, media_attachments)); } + // Send notifications for mentioned local users + for (const mention of mentions ?? []) { + if (mention.instanceId === null) { + await db.insert(notification).values({ + accountId: author.id, + notifiedId: mention.id, + type: "mention", + statusId: newStatus.id, + }); + } + } + return ( (await findFirstStatuses({ where: (status, { eq }) => eq(status.id, newStatus.id), @@ -1146,6 +1159,18 @@ export const editStatus = async ( .execute(); } + // Send notifications for mentioned local users + for (const mention of mentions ?? []) { + if (mention.instanceId === null) { + await db.insert(notification).values({ + accountId: statusToEdit.authorId, + notifiedId: mention.id, + type: "mention", + statusId: updated.id, + }); + } + } + // Set attachment parents await db .update(attachment) diff --git a/database/entities/User.ts b/database/entities/User.ts index 63a7820f..79774088 100644 --- a/database/entities/User.ts +++ b/database/entities/User.ts @@ -172,7 +172,7 @@ export const followRequestUser = async ( notify = false, languages: string[] = [], ): Promise> => { - const isRemote = follower.instanceId !== followee.instanceId; + const isRemote = followee.instanceId !== null; const updatedRelationship = ( await db @@ -226,9 +226,9 @@ export const followRequestUser = async ( } } else { await db.insert(notification).values({ - accountId: followee.id, + accountId: follower.id, type: followee.isLocked ? "follow_request" : "follow", - notifiedId: follower.id, + notifiedId: followee.id, }); } diff --git a/server/api/api/v1/notifications/index.test.ts b/server/api/api/v1/notifications/index.test.ts new file mode 100644 index 00000000..be6e0686 --- /dev/null +++ b/server/api/api/v1/notifications/index.test.ts @@ -0,0 +1,110 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { config } from "config-manager"; +import { + deleteOldTestUsers, + getTestStatuses, + getTestUsers, + sendTestRequest, +} from "~tests/utils"; +import { meta } from "./index"; +import type { Notification as APINotification } from "~types/mastodon/notification"; + +await deleteOldTestUsers(); + +const { users, tokens, deleteUsers } = await getTestUsers(2); +const timeline = (await getTestStatuses(40, users[0])).toReversed(); +// Create some test notifications: follow, favourite, reblog, mention +beforeAll(async () => { + await fetch( + new URL(`/api/v1/accounts/${users[0].id}/follow`, config.http.base_url), + { + method: "POST", + headers: { + Authorization: `Bearer ${tokens[1].accessToken}`, + }, + }, + ); + + await fetch( + new URL( + `/api/v1/statuses/${timeline[0].id}/favourite`, + config.http.base_url, + ), + { + method: "POST", + headers: { + Authorization: `Bearer ${tokens[1].accessToken}`, + }, + }, + ); + + await fetch( + new URL( + `/api/v1/statuses/${timeline[0].id}/reblog`, + config.http.base_url, + ), + { + method: "POST", + headers: { + Authorization: `Bearer ${tokens[1].accessToken}`, + }, + }, + ); + + await fetch(new URL("/api/v1/statuses", config.http.base_url), { + method: "POST", + headers: { + Authorization: `Bearer ${tokens[1].accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + status: `@${users[0].username} test mention`, + visibility: "direct", + federate: false, + }), + }); +}); + +afterAll(async () => { + await deleteUsers(); +}); + +// /api/v1/notifications +describe(meta.route, () => { + test("should return 401 if not authenticated", async () => { + const response = await sendTestRequest( + new Request(new URL(meta.route, config.http.base_url)), + ); + + expect(response.status).toBe(401); + }); + + test("should return 200 with notifications", async () => { + const response = await sendTestRequest( + new Request(new URL(meta.route, config.http.base_url), { + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + }), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("application/json"); + + const objects = (await response.json()) as APINotification[]; + + expect(objects.length).toBe(4); + for (const [index, notification] of objects.entries()) { + expect(notification.account).toBeDefined(); + expect(notification.account?.id).toBe(users[1].id); + expect(notification.created_at).toBeDefined(); + expect(notification.id).toBeDefined(); + expect(notification.type).toBeDefined(); + expect(notification.type).toBe( + ["follow", "favourite", "reblog", "mention"].toReversed()[ + index + ], + ); + } + }); +}); diff --git a/server/api/api/v1/notifications/index.ts b/server/api/api/v1/notifications/index.ts index fbc4badb..b00fc22b 100644 --- a/server/api/api/v1/notifications/index.ts +++ b/server/api/api/v1/notifications/index.ts @@ -47,7 +47,6 @@ export const schema = z.object({ "group_reblog", "group_favourite", "user_approved", - "update", ]) .array() .optional(), @@ -73,7 +72,6 @@ export const schema = z.object({ "group_reblog", "group_favourite", "user_approved", - "update", ]) .array() .optional(),