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, 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,

View file

@ -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<{

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -57,6 +57,13 @@
"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
} }
] ]
} }

View file

@ -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(

View file

@ -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,

View file

@ -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"],

View file

@ -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({

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, 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();

View file

@ -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,