refactor(database): ♻️ Simplify User and Note logic further

This commit is contained in:
Jesse Wierzbinski 2024-12-09 13:50:46 +01:00
parent a8541bdc44
commit 83399ba5f1
No known key found for this signature in database
6 changed files with 67 additions and 107 deletions

View file

@ -1,4 +1,5 @@
import { apiRoute, applyConfig, auth, jsonOrForm } from "@/api"; import { apiRoute, applyConfig, auth, jsonOrForm } from "@/api";
import { mergeAndDeduplicate } from "@/lib";
import { sanitizedHtmlStrip } from "@/sanitization"; import { sanitizedHtmlStrip } from "@/sanitization";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { Attachment, Emoji, User } from "@versia/kit/db"; import { Attachment, Emoji, User } from "@versia/kit/db";
@ -335,14 +336,10 @@ export default apiRoute((app) =>
await Emoji.parseFromText(sanitizedDisplayName); await Emoji.parseFromText(sanitizedDisplayName);
const noteEmojis = await Emoji.parseFromText(self.note); const noteEmojis = await Emoji.parseFromText(self.note);
const emojis = [ const emojis = mergeAndDeduplicate(
...displaynameEmojis, displaynameEmojis,
...noteEmojis, noteEmojis,
...fieldEmojis, fieldEmojis,
].filter(
// Deduplicate emojis
(emoji, index, self) =>
self.findIndex((e) => e.id === emoji.id) === index,
); );
// Connect emojis, if any // Connect emojis, if any

View file

@ -1,6 +1,6 @@
import { apiRoute, applyConfig, auth, jsonOrForm } from "@/api"; import { apiRoute, applyConfig, auth, jsonOrForm } from "@/api";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { Note, Notification } from "@versia/kit/db"; import { Note } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables"; import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
@ -138,12 +138,7 @@ export default apiRoute((app) =>
} }
if (note.author.isLocal() && user.isLocal()) { if (note.author.isLocal() && user.isLocal()) {
await Notification.insert({ await note.author.createNotification("reblog", user, newReblog);
accountId: user.id,
notifiedId: note.author.id,
type: "reblog",
noteId: newReblog.data.reblogId,
});
} }
return context.json(await finalNewReblog.toApi(user), 201); return context.json(await finalNewReblog.toApi(user), 201);

View file

@ -1,5 +1,6 @@
import { idValidator } from "@/api"; import { idValidator } from "@/api";
import { localObjectUri } from "@/constants"; import { localObjectUri } from "@/constants";
import { mergeAndDeduplicate } from "@/lib.ts";
import { sanitizedHtmlStrip } from "@/sanitization"; import { sanitizedHtmlStrip } from "@/sanitization";
import { sentry } from "@/sentry"; import { sentry } from "@/sentry";
import { getLogger } from "@logtape/logtape"; import { getLogger } from "@logtape/logtape";
@ -13,7 +14,7 @@ import type {
Delete as VersiaDelete, Delete as VersiaDelete,
Note as VersiaNote, Note as VersiaNote,
} from "@versia/federation/types"; } from "@versia/federation/types";
import { Instance, Notification, db } from "@versia/kit/db"; import { Instance, db } from "@versia/kit/db";
import { import {
Attachments, Attachments,
EmojiToNote, EmojiToNote,
@ -364,14 +365,12 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
}, },
); );
const fusedUsers = [...mentionedUsers, ...usersThatCanSeePost]; const fusedUsers = mergeAndDeduplicate(
mentionedUsers,
const deduplicatedUsersById = fusedUsers.filter( usersThatCanSeePost,
(user, index, self) =>
index === self.findIndex((t) => t.id === user.id),
); );
return deduplicatedUsersById; return fusedUsers;
} }
public get author(): User { public get author(): User {
@ -452,22 +451,13 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
data.content["text/plain"]?.content ?? data.content["text/plain"]?.content ??
Object.entries(data.content)[0][1].content; Object.entries(data.content)[0][1].content;
const parsedMentions = [ const parsedMentions = mergeAndDeduplicate(
...(data.mentions ?? []), data.mentions ?? [],
...(await parseTextMentions(plaintextContent, data.author)), await parseTextMentions(plaintextContent, data.author),
// Deduplicate by .id
].filter(
(mention, index, self) =>
index === self.findIndex((t) => t.id === mention.id),
); );
const parsedEmojis = mergeAndDeduplicate(
const parsedEmojis = [ data.emojis ?? [],
...(data.emojis ?? []), await Emoji.parseFromText(plaintextContent),
...(await Emoji.parseFromText(plaintextContent)),
// Deduplicate by .id
].filter(
(emoji, index, self) =>
index === self.findIndex((t) => t.id === emoji.id),
); );
const htmlContent = await contentToHtml(data.content, parsedMentions); const htmlContent = await contentToHtml(data.content, parsedMentions);
@ -500,14 +490,13 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
await newNote.updateAttachments(data.mediaAttachments ?? []); await newNote.updateAttachments(data.mediaAttachments ?? []);
// Send notifications for mentioned local users // Send notifications for mentioned local users
for (const mention of parsedMentions ?? []) { for (const mention of parsedMentions) {
if (mention.isLocal()) { if (mention.isLocal()) {
await Notification.insert({ await mention.createNotification(
accountId: data.author.id, "mention",
notifiedId: mention.id, data.author,
type: "mention", newNote,
noteId: newNote.id, );
});
} }
} }
@ -540,26 +529,15 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
Object.entries(data.content)[0][1].content) Object.entries(data.content)[0][1].content)
: undefined; : undefined;
const parsedMentions = [ const parsedMentions = mergeAndDeduplicate(
...(data.mentions ?? []), data.mentions ?? [],
...(plaintextContent plaintextContent
? await parseTextMentions(plaintextContent, data.author) ? await parseTextMentions(plaintextContent, data.author)
: []), : [],
// Deduplicate by .id
].filter(
(mention, index, self) =>
index === self.findIndex((t) => t.id === mention.id),
); );
const parsedEmojis = mergeAndDeduplicate(
const parsedEmojis = [ data.emojis ?? [],
...(data.emojis ?? []), plaintextContent ? await Emoji.parseFromText(plaintextContent) : [],
...(plaintextContent
? await Emoji.parseFromText(plaintextContent)
: []),
// Deduplicate by .id
].filter(
(emoji, index, self) =>
index === self.findIndex((t) => t.id === emoji.id),
); );
const htmlContent = data.content const htmlContent = data.content
@ -608,18 +586,12 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
return; return;
} }
// Fuse and deduplicate
const fusedEmojis = emojis.filter(
(emoji, index, self) =>
index === self.findIndex((t) => t.id === emoji.id),
);
// Connect emojis // Connect emojis
await db await db
.delete(EmojiToNote) .delete(EmojiToNote)
.where(eq(EmojiToNote.noteId, this.data.id)); .where(eq(EmojiToNote.noteId, this.data.id));
await db.insert(EmojiToNote).values( await db.insert(EmojiToNote).values(
fusedEmojis.map((emoji) => ({ emojis.map((emoji) => ({
emojiId: emoji.id, emojiId: emoji.id,
noteId: this.data.id, noteId: this.data.id,
})), })),

View file

@ -275,11 +275,10 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
senderId: this.id, senderId: this.id,
}); });
} else { } else {
await Notification.insert({ await otherUser.createNotification(
accountId: this.id, otherUser.data.isLocked ? "follow_request" : "follow",
type: otherUser.data.isLocked ? "follow_request" : "follow", this,
notifiedId: otherUser.id, );
});
} }
return foundRelationship; return foundRelationship;
@ -295,20 +294,6 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
recipientId: followee.id, recipientId: followee.id,
senderId: this.id, senderId: this.id,
}); });
} else if (!this.data.isLocked) {
if (relationship.data.following) {
await Notification.insert({
accountId: followee.id,
type: "unfollow",
notifiedId: this.id,
});
} else {
await Notification.insert({
accountId: followee.id,
type: "cancel-follow",
notifiedId: this.id,
});
}
} }
await relationship.update({ await relationship.update({
@ -526,12 +511,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
if (this.isLocal() && note.author.isLocal()) { if (this.isLocal() && note.author.isLocal()) {
// Notify the user that their post has been favourited // Notify the user that their post has been favourited
await Notification.insert({ await note.author.createNotification("favourite", this, note);
accountId: this.id,
type: "favourite",
notifiedId: note.author.id,
noteId: note.id,
});
} else if (this.isLocal() && note.author.isRemote()) { } else if (this.isLocal() && note.author.isRemote()) {
// Federate the like // Federate the like
this.federateToFollowers(newLike.toVersia()); this.federateToFollowers(newLike.toVersia());
@ -567,6 +547,19 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
} }
} }
public async createNotification(
type: "mention" | "follow_request" | "follow" | "favourite" | "reblog",
relatedUser: User,
note?: Note,
): Promise<void> {
await Notification.insert({
accountId: relatedUser.id,
type,
notifiedId: this.id,
noteId: note?.id ?? null,
});
}
public async clearAllNotifications(): Promise<void> { public async clearAllNotifications(): Promise<void> {
await db await db
.update(Notifications) .update(Notifications)

View file

@ -15,14 +15,7 @@ import type {
Note as VersiaNote, Note as VersiaNote,
User as VersiaUser, User as VersiaUser,
} from "@versia/federation/types"; } from "@versia/federation/types";
import { import { Instance, Like, Note, Relationship, User } from "@versia/kit/db";
Instance,
Like,
Note,
Notification,
Relationship,
User,
} from "@versia/kit/db";
import { Likes, Notes } from "@versia/kit/tables"; import { Likes, Notes } from "@versia/kit/tables";
import type { SocketAddress } from "bun"; import type { SocketAddress } from "bun";
import chalk from "chalk"; import chalk from "chalk";
@ -340,11 +333,10 @@ export class InboxProcessor {
languages: [], languages: [],
}); });
await Notification.insert({ await followee.createNotification(
accountId: author.id, followee.data.isLocked ? "follow_request" : "follow",
type: followee.data.isLocked ? "follow_request" : "follow", author,
notifiedId: followee.id, );
});
if (!followee.data.isLocked) { if (!followee.data.isLocked) {
await followee.sendFollowAccept(author); await followee.sendFollowAccept(author);

11
utils/lib.ts Normal file
View file

@ -0,0 +1,11 @@
type ElementWithId = { id: string };
export const mergeAndDeduplicate = <T extends ElementWithId>(
...elements: T[][]
): T[] =>
elements
.reduce((acc, val) => acc.concat(val), [])
.filter(
(element, index, self) =>
index === self.findIndex((t) => t.id === element.id),
);