mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
feat(api): ✨ Add dismiss, id and clear API endpoints for notifications
This commit is contained in:
parent
47133ac3fe
commit
0ca8000186
|
|
@ -32,16 +32,16 @@ import {
|
||||||
emojiToStatus,
|
emojiToStatus,
|
||||||
instance,
|
instance,
|
||||||
type like,
|
type like,
|
||||||
|
notification,
|
||||||
status,
|
status,
|
||||||
statusToMentions,
|
statusToMentions,
|
||||||
user,
|
user,
|
||||||
notification,
|
|
||||||
} from "~drizzle/schema";
|
} from "~drizzle/schema";
|
||||||
import { LogLevel } from "~packages/log-manager";
|
import { LogLevel } from "~packages/log-manager";
|
||||||
import type { Note } from "~types/lysand/Object";
|
import type { Note } from "~types/lysand/Object";
|
||||||
import type { Attachment as APIAttachment } from "~types/mastodon/attachment";
|
import type { Attachment as APIAttachment } from "~types/mastodon/attachment";
|
||||||
import type { Status as APIStatus } from "~types/mastodon/status";
|
import type { Status as APIStatus } from "~types/mastodon/status";
|
||||||
import { applicationToAPI, type Application } from "./Application";
|
import { type Application, applicationToAPI } from "./Application";
|
||||||
import {
|
import {
|
||||||
attachmentFromLysand,
|
attachmentFromLysand,
|
||||||
attachmentToAPI,
|
attachmentToAPI,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
import { LogLevel } from "~packages/log-manager";
|
import { LogLevel } from "~packages/log-manager";
|
||||||
import type { Account as APIAccount } from "~types/mastodon/account";
|
import type { Account as APIAccount } from "~types/mastodon/account";
|
||||||
import type { Source as APISource } from "~types/mastodon/source";
|
import type { Source as APISource } from "~types/mastodon/source";
|
||||||
|
import type { Application } from "./Application";
|
||||||
import {
|
import {
|
||||||
type EmojiWithInstance,
|
type EmojiWithInstance,
|
||||||
emojiToAPI,
|
emojiToAPI,
|
||||||
|
|
@ -28,7 +29,6 @@ import { objectToInboxRequest } from "./Federation";
|
||||||
import { addInstanceIfNotExists } from "./Instance";
|
import { addInstanceIfNotExists } from "./Instance";
|
||||||
import { createNewRelationship } from "./Relationship";
|
import { createNewRelationship } from "./Relationship";
|
||||||
import type { Token } from "./Token";
|
import type { Token } from "./Token";
|
||||||
import type { Application } from "./Application";
|
|
||||||
|
|
||||||
export type User = InferSelectModel<typeof user> & {
|
export type User = InferSelectModel<typeof user> & {
|
||||||
endpoints?: Partial<{
|
endpoints?: Partial<{
|
||||||
|
|
|
||||||
1
drizzle/0008_flawless_brother_voodoo.sql
Normal file
1
drizzle/0008_flawless_brother_voodoo.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE "Notification" ADD COLUMN "dismissed" boolean DEFAULT false NOT NULL;
|
||||||
File diff suppressed because it is too large
Load diff
1582
drizzle/meta/0008_snapshot.json
Normal file
1582
drizzle/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,62 +1,69 @@
|
||||||
{
|
{
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"dialect": "pg",
|
"dialect": "pg",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1712805159664,
|
"when": 1712805159664,
|
||||||
"tag": "0000_illegal_living_lightning",
|
"tag": "0000_illegal_living_lightning",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713055774123,
|
"when": 1713055774123,
|
||||||
"tag": "0001_salty_night_thrasher",
|
"tag": "0001_salty_night_thrasher",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 2,
|
"idx": 2,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713056370431,
|
"when": 1713056370431,
|
||||||
"tag": "0002_stiff_ares",
|
"tag": "0002_stiff_ares",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 3,
|
"idx": 3,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713056528340,
|
"when": 1713056528340,
|
||||||
"tag": "0003_spicy_arachne",
|
"tag": "0003_spicy_arachne",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 4,
|
"idx": 4,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713056712218,
|
"when": 1713056712218,
|
||||||
"tag": "0004_burly_lockjaw",
|
"tag": "0004_burly_lockjaw",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 5,
|
"idx": 5,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713056917973,
|
"when": 1713056917973,
|
||||||
"tag": "0005_sleepy_puma",
|
"tag": "0005_sleepy_puma",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 6,
|
"idx": 6,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713057159867,
|
"when": 1713057159867,
|
||||||
"tag": "0006_messy_network",
|
"tag": "0006_messy_network",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 7,
|
"idx": 7,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713227918208,
|
"when": 1713227918208,
|
||||||
"tag": "0007_naive_sleeper",
|
"tag": "0007_naive_sleeper",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1713246700119,
|
||||||
|
"tag": "0008_flawless_brother_voodoo",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +195,7 @@ export const notification = pgTable("Notification", {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
onUpdate: "cascade",
|
onUpdate: "cascade",
|
||||||
}),
|
}),
|
||||||
|
dismissed: boolean("dismissed").default(false).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const status = pgTable(
|
export const status = pgTable(
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { redirect } from "@response";
|
||||||
import { config } from "config-manager";
|
import { config } from "config-manager";
|
||||||
|
import { retrieveUserFromToken, userToAPI } from "~database/entities/User";
|
||||||
import type { LogManager, MultiLogManager } from "~packages/log-manager";
|
import type { LogManager, MultiLogManager } from "~packages/log-manager";
|
||||||
import { languages } from "./glitch-languages";
|
import { languages } from "./glitch-languages";
|
||||||
import { redirect } from "@response";
|
|
||||||
import { retrieveUserFromToken, userToAPI } from "~database/entities/User";
|
|
||||||
|
|
||||||
export const handleGlitchRequest = async (
|
export const handleGlitchRequest = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { z } from "zod";
|
||||||
import { TokenType } from "~database/entities/Token";
|
import { TokenType } from "~database/entities/Token";
|
||||||
import { findFirstUser } from "~database/entities/User";
|
import { findFirstUser } from "~database/entities/User";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { token } from "~drizzle/schema";
|
import { token } from "~drizzle/schema";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { z } from "zod";
|
||||||
import { TokenType } from "~database/entities/Token";
|
import { TokenType } from "~database/entities/Token";
|
||||||
import { findFirstUser } from "~database/entities/User";
|
import { findFirstUser } from "~database/entities/User";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { token } from "~drizzle/schema";
|
import { token } from "~drizzle/schema";
|
||||||
import { z } from "zod";
|
|
||||||
import { config } from "~packages/config-manager";
|
import { config } from "~packages/config-manager";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
|
|
|
||||||
93
server/api/api/v1/notifications/[id]/dismiss.test.ts
Normal file
93
server/api/api/v1/notifications/[id]/dismiss.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
37
server/api/api/v1/notifications/[id]/dismiss.ts
Normal file
37
server/api/api/v1/notifications/[id]/dismiss.ts
Normal 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({});
|
||||||
|
});
|
||||||
117
server/api/api/v1/notifications/[id]/index.test.ts
Normal file
117
server/api/api/v1/notifications/[id]/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
37
server/api/api/v1/notifications/[id]/index.ts
Normal file
37
server/api/api/v1/notifications/[id]/index.ts
Normal 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);
|
||||||
|
});
|
||||||
79
server/api/api/v1/notifications/clear/index.test.ts
Normal file
79
server/api/api/v1/notifications/clear/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
32
server/api/api/v1/notifications/clear/index.ts
Normal file
32
server/api/api/v1/notifications/clear/index.ts
Normal 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({});
|
||||||
|
});
|
||||||
|
|
@ -6,8 +6,8 @@ import {
|
||||||
getTestUsers,
|
getTestUsers,
|
||||||
sendTestRequest,
|
sendTestRequest,
|
||||||
} from "~tests/utils";
|
} from "~tests/utils";
|
||||||
import { meta } from "./index";
|
|
||||||
import type { Notification as APINotification } from "~types/mastodon/notification";
|
import type { Notification as APINotification } from "~types/mastodon/notification";
|
||||||
|
import { meta } from "./index";
|
||||||
|
|
||||||
await deleteOldTestUsers();
|
await deleteOldTestUsers();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export const meta = applyConfig({
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
|
oauthPermissions: ["read:notifications"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -115,6 +116,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
: undefined,
|
: undefined,
|
||||||
min_id ? gt(notification.id, min_id) : undefined,
|
min_id ? gt(notification.id, min_id) : undefined,
|
||||||
eq(notification.notifiedId, user.id),
|
eq(notification.notifiedId, user.id),
|
||||||
|
eq(notification.dismissed, false),
|
||||||
account_id
|
account_id
|
||||||
? eq(notification.accountId, account_id)
|
? eq(notification.accountId, account_id)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue