feat(api): Add dismiss, id and clear API endpoints for notifications

This commit is contained in:
Jesse Wierzbinski 2024-04-15 20:00:40 -10:00
parent 47133ac3fe
commit 0ca8000186
No known key found for this signature in database
18 changed files with 3621 additions and 1811 deletions

View file

@ -32,16 +32,16 @@ import {
emojiToStatus,
instance,
type like,
notification,
status,
statusToMentions,
user,
notification,
} from "~drizzle/schema";
import { LogLevel } from "~packages/log-manager";
import type { Note } from "~types/lysand/Object";
import type { Attachment as APIAttachment } from "~types/mastodon/attachment";
import type { Status as APIStatus } from "~types/mastodon/status";
import { applicationToAPI, type Application } from "./Application";
import { type Application, applicationToAPI } from "./Application";
import {
attachmentFromLysand,
attachmentToAPI,

View file

@ -18,6 +18,7 @@ import {
import { LogLevel } from "~packages/log-manager";
import type { Account as APIAccount } from "~types/mastodon/account";
import type { Source as APISource } from "~types/mastodon/source";
import type { Application } from "./Application";
import {
type EmojiWithInstance,
emojiToAPI,
@ -28,7 +29,6 @@ import { objectToInboxRequest } from "./Federation";
import { addInstanceIfNotExists } from "./Instance";
import { createNewRelationship } from "./Relationship";
import type { Token } from "./Token";
import type { Application } from "./Application";
export type User = InferSelectModel<typeof user> & {
endpoints?: Partial<{

View file

@ -0,0 +1 @@
ALTER TABLE "Notification" ADD COLUMN "dismissed" boolean DEFAULT false NOT NULL;

View file

@ -61,9 +61,7 @@
"indexes": {
"Application_client_id_index": {
"name": "Application_client_id_index",
"columns": [
"client_id"
],
"columns": ["client_id"],
"isUnique": true
}
},
@ -167,12 +165,8 @@
"name": "Attachment_statusId_Status_id_fk",
"tableFrom": "Attachment",
"tableTo": "Status",
"columnsFrom": [
"statusId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["statusId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -234,12 +228,8 @@
"name": "Emoji_instanceId_Instance_id_fk",
"tableFrom": "Emoji",
"tableTo": "Instance",
"columnsFrom": [
"instanceId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["instanceId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -267,17 +257,12 @@
"indexes": {
"EmojiToStatus_emojiId_statusId_index": {
"name": "EmojiToStatus_emojiId_statusId_index",
"columns": [
"emojiId",
"statusId"
],
"columns": ["emojiId", "statusId"],
"isUnique": true
},
"EmojiToStatus_statusId_index": {
"name": "EmojiToStatus_statusId_index",
"columns": [
"statusId"
],
"columns": ["statusId"],
"isUnique": false
}
},
@ -286,12 +271,8 @@
"name": "EmojiToStatus_emojiId_Emoji_id_fk",
"tableFrom": "EmojiToStatus",
"tableTo": "Emoji",
"columnsFrom": [
"emojiId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["emojiId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -299,12 +280,8 @@
"name": "EmojiToStatus_statusId_Status_id_fk",
"tableFrom": "EmojiToStatus",
"tableTo": "Status",
"columnsFrom": [
"statusId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["statusId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -332,17 +309,12 @@
"indexes": {
"EmojiToUser_emojiId_userId_index": {
"name": "EmojiToUser_emojiId_userId_index",
"columns": [
"emojiId",
"userId"
],
"columns": ["emojiId", "userId"],
"isUnique": true
},
"EmojiToUser_userId_index": {
"name": "EmojiToUser_userId_index",
"columns": [
"userId"
],
"columns": ["userId"],
"isUnique": false
}
},
@ -351,12 +323,8 @@
"name": "EmojiToUser_emojiId_Emoji_id_fk",
"tableFrom": "EmojiToUser",
"tableTo": "Emoji",
"columnsFrom": [
"emojiId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["emojiId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -364,12 +332,8 @@
"name": "EmojiToUser_userId_User_id_fk",
"tableFrom": "EmojiToUser",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -421,12 +385,8 @@
"name": "Flag_flaggeStatusId_Status_id_fk",
"tableFrom": "Flag",
"tableTo": "Status",
"columnsFrom": [
"flaggeStatusId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["flaggeStatusId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -434,12 +394,8 @@
"name": "Flag_flaggedUserId_User_id_fk",
"tableFrom": "Flag",
"tableTo": "User",
"columnsFrom": [
"flaggedUserId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["flaggedUserId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -532,12 +488,8 @@
"name": "Like_likerId_User_id_fk",
"tableFrom": "Like",
"tableTo": "User",
"columnsFrom": [
"likerId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["likerId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -545,12 +497,8 @@
"name": "Like_likedId_Status_id_fk",
"tableFrom": "Like",
"tableTo": "Status",
"columnsFrom": [
"likedId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["likedId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -616,16 +564,12 @@
"indexes": {
"LysandObject_remote_id_index": {
"name": "LysandObject_remote_id_index",
"columns": [
"remote_id"
],
"columns": ["remote_id"],
"isUnique": true
},
"LysandObject_uri_index": {
"name": "LysandObject_uri_index",
"columns": [
"uri"
],
"columns": ["uri"],
"isUnique": true
}
},
@ -634,12 +578,8 @@
"name": "LysandObject_authorId_LysandObject_id_fk",
"tableFrom": "LysandObject",
"tableTo": "LysandObject",
"columnsFrom": [
"authorId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["authorId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -696,12 +636,8 @@
"name": "ModNote_notedStatusId_Status_id_fk",
"tableFrom": "ModNote",
"tableTo": "Status",
"columnsFrom": [
"notedStatusId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["notedStatusId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -709,12 +645,8 @@
"name": "ModNote_notedUserId_User_id_fk",
"tableFrom": "ModNote",
"tableTo": "User",
"columnsFrom": [
"notedUserId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["notedUserId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -722,12 +654,8 @@
"name": "ModNote_modId_User_id_fk",
"tableFrom": "ModNote",
"tableTo": "User",
"columnsFrom": [
"modId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["modId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -784,12 +712,8 @@
"name": "ModTag_taggedStatusId_Status_id_fk",
"tableFrom": "ModTag",
"tableTo": "Status",
"columnsFrom": [
"taggedStatusId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["taggedStatusId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -797,12 +721,8 @@
"name": "ModTag_taggedUserId_User_id_fk",
"tableFrom": "ModTag",
"tableTo": "User",
"columnsFrom": [
"taggedUserId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["taggedUserId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -810,12 +730,8 @@
"name": "ModTag_modId_User_id_fk",
"tableFrom": "ModTag",
"tableTo": "User",
"columnsFrom": [
"modId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["modId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -872,12 +788,8 @@
"name": "Notification_notifiedId_User_id_fk",
"tableFrom": "Notification",
"tableTo": "User",
"columnsFrom": [
"notifiedId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["notifiedId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -885,12 +797,8 @@
"name": "Notification_accountId_User_id_fk",
"tableFrom": "Notification",
"tableTo": "User",
"columnsFrom": [
"accountId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["accountId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -898,12 +806,8 @@
"name": "Notification_statusId_Status_id_fk",
"tableFrom": "Notification",
"tableTo": "Status",
"columnsFrom": [
"statusId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["statusId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -947,12 +851,8 @@
"name": "OpenIdAccount_userId_User_id_fk",
"tableFrom": "OpenIdAccount",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "cascade"
}
@ -996,12 +896,8 @@
"name": "OpenIdLoginFlow_applicationId_Application_id_fk",
"tableFrom": "OpenIdLoginFlow",
"tableTo": "Application",
"columnsFrom": [
"applicationId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["applicationId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -1131,12 +1027,8 @@
"name": "Relationship_ownerId_User_id_fk",
"tableFrom": "Relationship",
"tableTo": "User",
"columnsFrom": [
"ownerId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["ownerId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -1144,12 +1036,8 @@
"name": "Relationship_subjectId_User_id_fk",
"tableFrom": "Relationship",
"tableTo": "User",
"columnsFrom": [
"subjectId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["subjectId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -1262,9 +1150,7 @@
"indexes": {
"Status_uri_index": {
"name": "Status_uri_index",
"columns": [
"uri"
],
"columns": ["uri"],
"isUnique": true
}
},
@ -1273,12 +1159,8 @@
"name": "Status_authorId_User_id_fk",
"tableFrom": "Status",
"tableTo": "User",
"columnsFrom": [
"authorId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["authorId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -1286,12 +1168,8 @@
"name": "Status_applicationId_Application_id_fk",
"tableFrom": "Status",
"tableTo": "Application",
"columnsFrom": [
"applicationId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["applicationId"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "cascade"
},
@ -1299,12 +1177,8 @@
"name": "Status_reblogId_Status_id_fk",
"tableFrom": "Status",
"tableTo": "Status",
"columnsFrom": [
"reblogId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["reblogId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -1312,12 +1186,8 @@
"name": "Status_inReplyToPostId_Status_id_fk",
"tableFrom": "Status",
"tableTo": "Status",
"columnsFrom": [
"inReplyToPostId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["inReplyToPostId"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "cascade"
},
@ -1325,12 +1195,8 @@
"name": "Status_quotingPostId_Status_id_fk",
"tableFrom": "Status",
"tableTo": "Status",
"columnsFrom": [
"quotingPostId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["quotingPostId"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "cascade"
}
@ -1358,17 +1224,12 @@
"indexes": {
"StatusToMentions_statusId_userId_index": {
"name": "StatusToMentions_statusId_userId_index",
"columns": [
"statusId",
"userId"
],
"columns": ["statusId", "userId"],
"isUnique": true
},
"StatusToMentions_userId_index": {
"name": "StatusToMentions_userId_index",
"columns": [
"userId"
],
"columns": ["userId"],
"isUnique": false
}
},
@ -1377,12 +1238,8 @@
"name": "StatusToMentions_statusId_Status_id_fk",
"tableFrom": "StatusToMentions",
"tableTo": "Status",
"columnsFrom": [
"statusId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["statusId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -1390,12 +1247,8 @@
"name": "StatusToMentions_userId_User_id_fk",
"tableFrom": "StatusToMentions",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -1464,12 +1317,8 @@
"name": "Token_userId_User_id_fk",
"tableFrom": "Token",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -1477,12 +1326,8 @@
"name": "Token_applicationId_Application_id_fk",
"tableFrom": "Token",
"tableTo": "Application",
"columnsFrom": [
"applicationId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["applicationId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -1639,23 +1484,17 @@
"indexes": {
"User_uri_index": {
"name": "User_uri_index",
"columns": [
"uri"
],
"columns": ["uri"],
"isUnique": true
},
"User_username_index": {
"name": "User_username_index",
"columns": [
"username"
],
"columns": ["username"],
"isUnique": true
},
"User_email_index": {
"name": "User_email_index",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@ -1664,12 +1503,8 @@
"name": "User_instanceId_Instance_id_fk",
"tableFrom": "User",
"tableTo": "Instance",
"columnsFrom": [
"instanceId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["instanceId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@ -1697,17 +1532,12 @@
"indexes": {
"UserToPinnedNotes_userId_statusId_index": {
"name": "UserToPinnedNotes_userId_statusId_index",
"columns": [
"userId",
"statusId"
],
"columns": ["userId", "statusId"],
"isUnique": true
},
"UserToPinnedNotes_statusId_index": {
"name": "UserToPinnedNotes_statusId_index",
"columns": [
"statusId"
],
"columns": ["statusId"],
"isUnique": false
}
},
@ -1716,12 +1546,8 @@
"name": "UserToPinnedNotes_userId_Status_id_fk",
"tableFrom": "UserToPinnedNotes",
"tableTo": "Status",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@ -1729,12 +1555,8 @@
"name": "UserToPinnedNotes_statusId_User_id_fk",
"tableFrom": "UserToPinnedNotes",
"tableTo": "User",
"columnsFrom": [
"statusId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["statusId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}

File diff suppressed because it is too large Load diff

View file

@ -57,6 +57,13 @@
"when": 1713227918208,
"tag": "0007_naive_sleeper",
"breakpoints": true
},
{
"idx": 8,
"version": "5",
"when": 1713246700119,
"tag": "0008_flawless_brother_voodoo",
"breakpoints": true
}
]
}

View file

@ -195,6 +195,7 @@ export const notification = pgTable("Notification", {
onDelete: "cascade",
onUpdate: "cascade",
}),
dismissed: boolean("dismissed").default(false).notNull(),
});
export const status = pgTable(

View file

@ -1,9 +1,9 @@
import { join } from "node:path";
import { redirect } from "@response";
import { config } from "config-manager";
import { retrieveUserFromToken, userToAPI } from "~database/entities/User";
import type { LogManager, MultiLogManager } from "~packages/log-manager";
import { languages } from "./glitch-languages";
import { redirect } from "@response";
import { retrieveUserFromToken, userToAPI } from "~database/entities/User";
export const handleGlitchRequest = async (
req: Request,

View file

@ -1,10 +1,10 @@
import { randomBytes } from "node:crypto";
import { apiRoute, applyConfig } from "@api";
import { z } from "zod";
import { TokenType } from "~database/entities/Token";
import { findFirstUser } from "~database/entities/User";
import { db } from "~drizzle/db";
import { token } from "~drizzle/schema";
import { z } from "zod";
export const meta = applyConfig({
allowedMethods: ["POST"],

View file

@ -1,10 +1,10 @@
import { randomBytes } from "node:crypto";
import { apiRoute, applyConfig } from "@api";
import { z } from "zod";
import { TokenType } from "~database/entities/Token";
import { findFirstUser } from "~database/entities/User";
import { db } from "~drizzle/db";
import { token } from "~drizzle/schema";
import { z } from "zod";
import { config } from "~packages/config-manager";
export const meta = applyConfig({

View file

@ -0,0 +1,93 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager";
import {
deleteOldTestUsers,
getTestUsers,
sendTestRequest,
} from "~tests/utils";
import type { Notification as APINotification } from "~types/mastodon/notification";
import { meta } from "./dismiss";
await deleteOldTestUsers();
const { users, tokens, deleteUsers } = await getTestUsers(2);
let notifications: APINotification[] = [];
// 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}`,
},
},
);
notifications = await fetch(
new URL("/api/v1/notifications", config.http.base_url),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
).then((r) => r.json());
expect(notifications.length).toBe(1);
});
afterAll(async () => {
await deleteUsers();
});
// /api/v1/notifications/:id/dismiss
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), {
method: "POST",
}),
);
expect(response.status).toBe(401);
});
test("should dismiss notification", async () => {
const response = await sendTestRequest(
new Request(
new URL(
meta.route.replace(":id", notifications[0].id),
config.http.base_url,
),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(200);
});
test("should not display dismissed notification", async () => {
const response = await sendTestRequest(
new Request(
new URL("/api/v1/notifications", config.http.base_url),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(200);
const output = await response.json();
expect(output.length).toBe(0);
});
});

View file

@ -0,0 +1,37 @@
import { apiRoute, applyConfig, idValidator } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { eq } from "drizzle-orm";
import { db } from "~drizzle/db";
import { notification } from "~drizzle/schema";
export const meta = applyConfig({
allowedMethods: ["POST"],
route: "/api/v1/notifications/:id/dismiss",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
oauthPermissions: ["write:notifications"],
},
});
export default apiRoute(async (req, matchedRoute, extraData) => {
const id = matchedRoute.params.id;
if (!id.match(idValidator)) {
return errorResponse("Invalid ID, must be of type UUIDv7", 404);
}
const { user } = extraData.auth;
if (!user) return errorResponse("Unauthorized", 401);
await db
.update(notification)
.set({
dismissed: true,
})
.where(eq(notification.id, id));
return jsonResponse({});
});

View file

@ -0,0 +1,117 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager";
import {
deleteOldTestUsers,
getTestUsers,
sendTestRequest,
} from "~tests/utils";
import type { Notification as APINotification } from "~types/mastodon/notification";
import { meta } from "./index";
await deleteOldTestUsers();
const { users, tokens, deleteUsers } = await getTestUsers(2);
let notifications: APINotification[] = [];
// 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}`,
},
},
);
notifications = await fetch(
new URL("/api/v1/notifications", config.http.base_url),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
).then((r) => r.json());
expect(notifications.length).toBe(1);
});
afterAll(async () => {
await deleteUsers();
});
// /api/v1/notifications/:id
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 404 if ID is invalid", async () => {
const response = await sendTestRequest(
new Request(
new URL(
meta.route.replace(":id", "invalid"),
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(404);
});
test("should return 404 if notification not found", async () => {
const response = await sendTestRequest(
new Request(
new URL(
meta.route.replace(
":id",
"00000000-0000-0000-0000-000000000000",
),
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(404);
});
test("should return notification", async () => {
const response = await sendTestRequest(
new Request(
new URL(
meta.route.replace(":id", notifications[0].id),
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(200);
const notification = (await response.json()) as APINotification;
expect(notification).toBeDefined();
expect(notification.id).toBe(notifications[0].id);
expect(notification.type).toBe("follow");
expect(notification.account).toBeDefined();
expect(notification.account?.id).toBe(users[1].id);
});
});

View file

@ -0,0 +1,37 @@
import { apiRoute, applyConfig, idValidator } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { findManyNotifications } from "~database/entities/Notification";
export const meta = applyConfig({
allowedMethods: ["GET"],
route: "/api/v1/notifications/:id",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
oauthPermissions: ["read:notifications"],
},
});
export default apiRoute(async (req, matchedRoute, extraData) => {
const id = matchedRoute.params.id;
if (!id.match(idValidator)) {
return errorResponse("Invalid ID, must be of type UUIDv7", 404);
}
const { user } = extraData.auth;
if (!user) return errorResponse("Unauthorized", 401);
const notification = (
await findManyNotifications({
where: (notification, { eq }) => eq(notification.id, id),
limit: 1,
})
)[0];
if (!notification) return errorResponse("Notification not found", 404);
return jsonResponse(notification);
});

View file

@ -0,0 +1,79 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager";
import {
deleteOldTestUsers,
getTestUsers,
sendTestRequest,
} from "~tests/utils";
import type { Notification as APINotification } from "~types/mastodon/notification";
import { meta } from "./index";
await deleteOldTestUsers();
const { users, tokens, deleteUsers } = await getTestUsers(2);
let notifications: APINotification[] = [];
// 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}`,
},
},
);
notifications = await fetch(
new URL("/api/v1/notifications", config.http.base_url),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
).then((r) => r.json());
expect(notifications.length).toBe(1);
});
afterAll(async () => {
await deleteUsers();
});
// /api/v1/notifications/clear
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), {
method: "POST",
}),
);
expect(response.status).toBe(401);
});
test("should clear notifications", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
}),
);
expect(response.status).toBe(200);
const newNotifications = await fetch(
new URL("/api/v1/notifications", config.http.base_url),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
).then((r) => r.json());
expect(newNotifications.length).toBe(0);
});
});

View file

@ -0,0 +1,32 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { eq } from "drizzle-orm";
import { db } from "~drizzle/db";
import { notification } from "~drizzle/schema";
export const meta = applyConfig({
allowedMethods: ["POST"],
route: "/api/v1/notifications/clear",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
oauthPermissions: ["write:notifications"],
},
});
export default apiRoute(async (req, matchedRoute, extraData) => {
const { user } = extraData.auth;
if (!user) return errorResponse("Unauthorized", 401);
await db
.update(notification)
.set({
dismissed: true,
})
.where(eq(notification.notifiedId, user.id));
return jsonResponse({});
});

View file

@ -6,8 +6,8 @@ import {
getTestUsers,
sendTestRequest,
} from "~tests/utils";
import { meta } from "./index";
import type { Notification as APINotification } from "~types/mastodon/notification";
import { meta } from "./index";
await deleteOldTestUsers();

View file

@ -17,6 +17,7 @@ export const meta = applyConfig({
},
auth: {
required: true,
oauthPermissions: ["read:notifications"],
},
});
@ -115,6 +116,7 @@ export default apiRoute<typeof meta, typeof schema>(
: undefined,
min_id ? gt(notification.id, min_id) : undefined,
eq(notification.notifiedId, user.id),
eq(notification.dismissed, false),
account_id
? eq(notification.accountId, account_id)
: undefined,