From f79b0bc999b4da6f86d57093f1bff4890ffbcab6 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 8 Apr 2025 18:13:30 +0200 Subject: [PATCH] refactor(federation): :fire: Refactor Note federation and creation code --- api/api/v1/accounts/[id]/refetch.ts | 2 +- .../follow_requests/[account_id]/authorize.ts | 2 +- .../v1/follow_requests/[account_id]/reject.ts | 2 +- api/api/v1/statuses/[id]/index.ts | 58 ++- api/api/v1/statuses/[id]/reblog.ts | 2 +- api/api/v1/statuses/index.ts | 63 ++- api/likes/[uuid]/index.ts | 2 +- api/notes/[uuid]/index.ts | 5 +- api/notes/[uuid]/quotes.ts | 5 +- api/notes/[uuid]/replies.ts | 5 +- api/users/[uuid]/index.ts | 2 +- api/users/[uuid]/outbox/index.ts | 2 +- classes/database/note.ts | 395 +++++------------- classes/database/reaction.ts | 6 +- classes/database/user.ts | 38 +- classes/functions/status.ts | 2 +- classes/queues/inbox.ts | 2 +- cli/user/refetch.ts | 2 +- drizzle/schema.ts | 2 +- 19 files changed, 243 insertions(+), 354 deletions(-) diff --git a/api/api/v1/accounts/[id]/refetch.ts b/api/api/v1/accounts/[id]/refetch.ts index c089c8a4..d096d53e 100644 --- a/api/api/v1/accounts/[id]/refetch.ts +++ b/api/api/v1/accounts/[id]/refetch.ts @@ -45,7 +45,7 @@ export default apiRoute((app) => async (context) => { const otherUser = context.get("user"); - if (otherUser.isLocal()) { + if (otherUser.local) { throw new ApiError(400, "Cannot refetch a local user"); } diff --git a/api/api/v1/follow_requests/[account_id]/authorize.ts b/api/api/v1/follow_requests/[account_id]/authorize.ts index 70acad50..29abd259 100644 --- a/api/api/v1/follow_requests/[account_id]/authorize.ts +++ b/api/api/v1/follow_requests/[account_id]/authorize.ts @@ -71,7 +71,7 @@ export default apiRoute((app) => ); // Check if accepting remote follow - if (account.isRemote()) { + if (account.remote) { // Federate follow accept await user.acceptFollowRequest(account); } diff --git a/api/api/v1/follow_requests/[account_id]/reject.ts b/api/api/v1/follow_requests/[account_id]/reject.ts index a1d8354e..a7e0bf7d 100644 --- a/api/api/v1/follow_requests/[account_id]/reject.ts +++ b/api/api/v1/follow_requests/[account_id]/reject.ts @@ -72,7 +72,7 @@ export default apiRoute((app) => ); // Check if rejecting remote follow - if (account.isRemote()) { + if (account.remote) { // Federate follow reject await user.rejectFollowRequest(account); } diff --git a/api/api/v1/statuses/[id]/index.ts b/api/api/v1/statuses/[id]/index.ts index ae699bb6..229a1b2c 100644 --- a/api/api/v1/statuses/[id]/index.ts +++ b/api/api/v1/statuses/[id]/index.ts @@ -5,6 +5,7 @@ import { jsonOrForm, withNoteParam, } from "@/api"; +import { sanitizedHtmlStrip } from "@/sanitization"; import { Attachment as AttachmentSchema, PollOption, @@ -13,12 +14,13 @@ import { zBoolean, } from "@versia/client/schemas"; import { RolePermission } from "@versia/client/schemas"; -import { Media } from "@versia/kit/db"; +import { Emoji, Media } from "@versia/kit/db"; import * as VersiaEntities from "@versia/sdk/entities"; import { describeRoute } from "hono-openapi"; import { resolver, validator } from "hono-openapi/zod"; import { z } from "zod"; import { ApiError } from "~/classes/errors/api-error"; +import { contentToHtml, parseTextMentions } from "~/classes/functions/status"; import { config } from "~/config.ts"; const schema = z @@ -226,22 +228,50 @@ export default apiRoute((app) => { ); } - const newNote = await note.updateFromData({ - author: user, - content: statusText - ? new VersiaEntities.TextContentFormat({ - [content_type]: { - content: statusText, - remote: false, - }, - }) + const sanitizedSpoilerText = spoiler_text + ? await sanitizedHtmlStrip(spoiler_text) + : undefined; + + const content = statusText + ? new VersiaEntities.TextContentFormat({ + [content_type]: { + content: statusText, + remote: false, + }, + }) + : undefined; + + const parsedMentions = statusText + ? await parseTextMentions(statusText, user) + : []; + + const parsedEmojis = statusText + ? await Emoji.parseFromText(statusText) + : []; + + await note.update({ + spoilerText: sanitizedSpoilerText, + sensitive, + content: content + ? await contentToHtml(content, parsedMentions) : undefined, - isSensitive: sensitive, - spoilerText: spoiler_text, - mediaAttachments: foundAttachments, }); - return context.json(await newNote.toApi(user), 200); + // Emojis, mentions, and attachments are stored in a different table, so update them there too + await note.updateEmojis(parsedEmojis); + await note.updateMentions(parsedMentions); + await note.updateAttachments(foundAttachments); + + await note.reload(); + + // Send notifications for mentioned local users + for (const mentioned of parsedMentions) { + if (mentioned.local) { + await mentioned.notify("mention", user, note); + } + } + + return context.json(await note.toApi(user), 200); }, ); }); diff --git a/api/api/v1/statuses/[id]/reblog.ts b/api/api/v1/statuses/[id]/reblog.ts index 433419e5..45253d0a 100644 --- a/api/api/v1/statuses/[id]/reblog.ts +++ b/api/api/v1/statuses/[id]/reblog.ts @@ -83,7 +83,7 @@ export default apiRoute((app) => throw new Error("Failed to reblog"); } - if (note.author.isLocal() && user.isLocal()) { + if (note.author.local && user.local) { await note.author.notify("reblog", user, newReblog); } diff --git a/api/api/v1/statuses/index.ts b/api/api/v1/statuses/index.ts index 8ea0180a..8669df4a 100644 --- a/api/api/v1/statuses/index.ts +++ b/api/api/v1/statuses/index.ts @@ -1,4 +1,5 @@ import { apiRoute, auth, handleZodError, jsonOrForm } from "@/api"; +import { sanitizedHtmlStrip } from "@/sanitization"; import { Attachment as AttachmentSchema, PollOption, @@ -7,12 +8,14 @@ import { zBoolean, } from "@versia/client/schemas"; import { RolePermission } from "@versia/client/schemas"; -import { Media, Note } from "@versia/kit/db"; +import { Emoji, Media, Note } from "@versia/kit/db"; import * as VersiaEntities from "@versia/sdk/entities"; +import { randomUUIDv7 } from "bun"; import { describeRoute } from "hono-openapi"; import { resolver, validator } from "hono-openapi/zod"; import { z } from "zod"; import { ApiError } from "~/classes/errors/api-error"; +import { contentToHtml, parseTextMentions } from "~/classes/functions/status"; import { config } from "~/config.ts"; const schema = z @@ -175,27 +178,59 @@ export default apiRoute((app) => ); } - const newNote = await Note.fromData({ - author: user, - content: new VersiaEntities.TextContentFormat({ - [content_type]: { - content: status ?? "", - remote: false, - }, - }), + const sanitizedSpoilerText = spoiler_text + ? await sanitizedHtmlStrip(spoiler_text) + : undefined; + + const content = status + ? new VersiaEntities.TextContentFormat({ + [content_type]: { + content: status, + remote: false, + }, + }) + : undefined; + + const parsedMentions = status + ? await parseTextMentions(status, user) + : []; + + const parsedEmojis = status + ? await Emoji.parseFromText(status) + : []; + + const newNote = await Note.insert({ + id: randomUUIDv7(), + authorId: user.id, visibility, - isSensitive: sensitive ?? false, - spoilerText: spoiler_text ?? "", - mediaAttachments: foundAttachments, + content: content + ? await contentToHtml(content, parsedMentions) + : undefined, + sensitive, + spoilerText: sanitizedSpoilerText, replyId: in_reply_to_id ?? undefined, - quoteId: quote_id ?? undefined, - application: application ?? undefined, + quotingId: quote_id ?? undefined, + applicationId: application?.id, }); + // Emojis, mentions, and attachments are stored in a different table, so update them there too + await newNote.updateEmojis(parsedEmojis); + await newNote.updateMentions(parsedMentions); + await newNote.updateAttachments(foundAttachments); + + await newNote.reload(); + if (!local_only) { await newNote.federateToUsers(); } + // Send notifications for mentioned local users + for (const mentioned of parsedMentions) { + if (mentioned.local) { + await mentioned.notify("mention", user, newNote); + } + } + return context.json(await newNote.toApi(user), 200); }, ), diff --git a/api/likes/[uuid]/index.ts b/api/likes/[uuid]/index.ts index 9e4df8f2..2527f6bf 100644 --- a/api/likes/[uuid]/index.ts +++ b/api/likes/[uuid]/index.ts @@ -59,7 +59,7 @@ export default apiRoute((app) => const liker = await User.fromId(like.data.likerId); - if (!liker || liker.isRemote()) { + if (!liker || liker.remote) { throw ApiError.accountNotFound(); } diff --git a/api/notes/[uuid]/index.ts b/api/notes/[uuid]/index.ts index b8d2f229..c680fc70 100644 --- a/api/notes/[uuid]/index.ts +++ b/api/notes/[uuid]/index.ts @@ -53,10 +53,7 @@ export default apiRoute((app) => ), ); - if ( - !(note && (await note.isViewableByUser(null))) || - note.isRemote() - ) { + if (!(note && (await note.isViewableByUser(null))) || note.remote) { throw ApiError.noteNotFound(); } diff --git a/api/notes/[uuid]/quotes.ts b/api/notes/[uuid]/quotes.ts index 1ebfaa36..cd896e51 100644 --- a/api/notes/[uuid]/quotes.ts +++ b/api/notes/[uuid]/quotes.ts @@ -63,10 +63,7 @@ export default apiRoute((app) => ), ); - if ( - !(note && (await note.isViewableByUser(null))) || - note.isRemote() - ) { + if (!(note && (await note.isViewableByUser(null))) || note.remote) { throw ApiError.noteNotFound(); } diff --git a/api/notes/[uuid]/replies.ts b/api/notes/[uuid]/replies.ts index e95b49e7..f2243ed5 100644 --- a/api/notes/[uuid]/replies.ts +++ b/api/notes/[uuid]/replies.ts @@ -61,10 +61,7 @@ export default apiRoute((app) => ), ); - if ( - !(note && (await note.isViewableByUser(null))) || - note.isRemote() - ) { + if (!(note && (await note.isViewableByUser(null))) || note.remote) { throw ApiError.noteNotFound(); } diff --git a/api/users/[uuid]/index.ts b/api/users/[uuid]/index.ts index 1dfdb3f9..63182daf 100644 --- a/api/users/[uuid]/index.ts +++ b/api/users/[uuid]/index.ts @@ -53,7 +53,7 @@ export default apiRoute((app) => throw ApiError.accountNotFound(); } - if (user.isRemote()) { + if (user.remote) { throw new ApiError(403, "User is not on this instance"); } diff --git a/api/users/[uuid]/outbox/index.ts b/api/users/[uuid]/outbox/index.ts index 50ae6925..a72e31ca 100644 --- a/api/users/[uuid]/outbox/index.ts +++ b/api/users/[uuid]/outbox/index.ts @@ -70,7 +70,7 @@ export default apiRoute((app) => throw new ApiError(404, "User not found"); } - if (author.isRemote()) { + if (author.remote) { throw new ApiError(403, "User is not on this instance"); } diff --git a/classes/database/note.ts b/classes/database/note.ts index f83cbacf..1aff1277 100644 --- a/classes/database/note.ts +++ b/classes/database/note.ts @@ -1,9 +1,7 @@ import { idValidator } from "@/api"; import { mergeAndDeduplicate } from "@/lib.ts"; import { sanitizedHtmlStrip } from "@/sanitization"; -import { sentry } from "@/sentry"; -import { getLogger } from "@logtape/logtape"; -import type { Status, Status as StatusSchema } from "@versia/client/schemas"; +import type { Status } from "@versia/client/schemas"; import { Instance, db } from "@versia/kit/db"; import { EmojiToNote, @@ -28,11 +26,7 @@ import { import { htmlToText } from "html-to-text"; import { createRegExp, exactly, global } from "magic-regexp"; import type { z } from "zod"; -import { - contentToHtml, - findManyNotes, - parseTextMentions, -} from "~/classes/functions/status"; +import { contentToHtml, findManyNotes } from "~/classes/functions/status"; import { config } from "~/config.ts"; import type { NonTextContentFormatSchema } from "~/packages/federation/schemas/contentformat.ts"; import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts"; @@ -306,179 +300,12 @@ export class Note extends BaseInterface { ); } - public isRemote(): boolean { - return this.author.isRemote(); + public get remote(): boolean { + return this.author.remote; } - /** - * Update a note from remote federated servers - * @returns The updated note - */ - public async updateFromRemote(): Promise { - if (!this.isRemote()) { - throw new Error("Cannot refetch a local note (it is not remote)"); - } - - const note = await User.federationRequester.fetchEntity( - this.getUri(), - VersiaEntities.Note, - ); - - const updated = await Note.fromVersia(note); - - if (!updated) { - throw new Error("Note not found after update"); - } - - this.data = updated.data; - - return this; - } - - /** - * Create a new note from user input - * @param data - The data to create the note from - * @returns The created note - */ - public static async fromData(data: { - author: User; - content: VersiaEntities.TextContentFormat; - visibility: z.infer; - isSensitive: boolean; - spoilerText: string; - emojis?: Emoji[]; - uri?: URL; - mentions?: User[]; - /** List of IDs of database Attachment objects */ - mediaAttachments?: Media[]; - replyId?: string; - quoteId?: string; - application?: Application; - }): Promise { - const plaintextContent = - data.content.data["text/plain"]?.content ?? - Object.entries(data.content.data)[0][1].content; - - const parsedMentions = mergeAndDeduplicate( - data.mentions ?? [], - await parseTextMentions(plaintextContent, data.author), - ); - const parsedEmojis = mergeAndDeduplicate( - data.emojis ?? [], - await Emoji.parseFromText(plaintextContent), - ); - - const htmlContent = await contentToHtml(data.content, parsedMentions); - - const newNote = await Note.insert({ - id: randomUUIDv7(), - authorId: data.author.id, - content: htmlContent, - contentSource: - data.content.data["text/plain"]?.content || - data.content.data["text/markdown"]?.content || - Object.entries(data.content.data)[0][1].content || - "", - contentType: "text/html", - visibility: data.visibility, - sensitive: data.isSensitive, - spoilerText: await sanitizedHtmlStrip(data.spoilerText), - uri: data.uri?.href || null, - replyId: data.replyId ?? null, - quotingId: data.quoteId ?? null, - applicationId: data.application?.id ?? null, - }); - - // Connect emojis - await newNote.updateEmojis(parsedEmojis); - - // Connect mentions - await newNote.updateMentions(parsedMentions); - - // Set attachment parents - await newNote.updateAttachments(data.mediaAttachments ?? []); - - // Send notifications for mentioned local users - for (const mention of parsedMentions) { - if (mention.isLocal()) { - await mention.notify("mention", data.author, newNote); - } - } - - await newNote.reload(data.author.id); - - return newNote; - } - - /** - * Update a note from user input - * @param data - The data to update the note from - * @returns The updated note - */ - public async updateFromData(data: { - author: User; - content?: VersiaEntities.TextContentFormat; - visibility?: z.infer; - isSensitive?: boolean; - spoilerText?: string; - emojis?: Emoji[]; - uri?: URL; - mentions?: User[]; - mediaAttachments?: Media[]; - replyId?: string; - quoteId?: string; - application?: Application; - }): Promise { - const plaintextContent = data.content - ? (data.content.data["text/plain"]?.content ?? - Object.entries(data.content.data)[0][1].content) - : undefined; - - const parsedMentions = mergeAndDeduplicate( - data.mentions ?? [], - plaintextContent - ? await parseTextMentions(plaintextContent, data.author) - : [], - ); - const parsedEmojis = mergeAndDeduplicate( - data.emojis ?? [], - plaintextContent ? await Emoji.parseFromText(plaintextContent) : [], - ); - - const htmlContent = data.content - ? await contentToHtml(data.content, parsedMentions) - : undefined; - - await this.update({ - content: htmlContent, - contentSource: data.content - ? data.content.data["text/plain"]?.content || - data.content.data["text/markdown"]?.content || - Object.entries(data.content.data)[0][1].content || - "" - : undefined, - contentType: "text/html", - visibility: data.visibility, - sensitive: data.isSensitive, - spoilerText: data.spoilerText, - uri: data.uri?.href, - replyId: data.replyId, - quotingId: data.quoteId, - applicationId: data.application?.id, - }); - - // Connect emojis - await this.updateEmojis(parsedEmojis); - - // Connect mentions - await this.updateMentions(parsedMentions); - - // Set attachment parents - await this.updateAttachments(data.mediaAttachments ?? []); - - await this.reload(data.author.id); - - return this; + public get local(): boolean { + return this.author.local; } /** @@ -558,7 +385,7 @@ export class Note extends BaseInterface { */ public static async resolve(uri: URL): Promise { // Check if note not already in database - const foundNote = await Note.fromSql(eq(Notes.uri, uri.toString())); + const foundNote = await Note.fromSql(eq(Notes.uri, uri.href)); if (foundNote) { return foundNote; @@ -577,118 +404,124 @@ export class Note extends BaseInterface { return await Note.fromId(uuid[0]); } - const note = await User.federationRequester.fetchEntity( - uri, - VersiaEntities.Note, - ); - - return Note.fromVersia(note); + return Note.fromVersia(uri); } /** - * Turns a Versia Note into a database note (saved) - * @param note Versia Note - * @param author Author of the note - * @param instance Instance of the note - * @returns The saved note + * Tries to fetch a Versia Note from the given URL. + * + * @param url The URL to fetch the note from */ - public static async fromVersia(note: VersiaEntities.Note): Promise { - const emojis: Emoji[] = []; - const logger = getLogger(["federation", "resolvers"]); + public static async fromVersia(url: URL): Promise; - const author = await User.resolve(note.data.author); - if (!author) { - throw new Error("Invalid object author"); - } + /** + * Takes a Versia Note representation, and serializes it to the database. + * + * If the note already exists, it will update it. + * @param versiaNote + */ + public static async fromVersia( + versiaNote: VersiaEntities.Note, + ): Promise; - const instance = await Instance.resolve(note.data.uri); - - for (const emoji of note.data.extensions?.["pub.versia:custom_emojis"] - ?.emojis ?? []) { - const resolvedEmoji = await Emoji.fetchFromRemote( - emoji, - instance, - ).catch((e) => { - logger.error`${e}`; - sentry?.captureException(e); - return null; - }); - - if (resolvedEmoji) { - emojis.push(resolvedEmoji); - } - } - - const attachments: Media[] = []; - - for (const attachment of note.attachments) { - const resolvedAttachment = await Media.fromVersia(attachment).catch( - (e) => { - logger.error`${e}`; - sentry?.captureException(e); - return null; - }, + public static async fromVersia( + versiaNote: VersiaEntities.Note | URL, + ): Promise { + if (versiaNote instanceof URL) { + // No bridge support for notes yet + const note = await User.federationRequester.fetchEntity( + versiaNote, + VersiaEntities.Note, ); - if (resolvedAttachment) { - attachments.push(resolvedAttachment); + return Note.fromVersia(note); + } + + const { + author: authorUrl, + created_at, + uri, + extensions, + group, + is_sensitive, + mentions: noteMentions, + quotes, + replies_to, + subject, + } = versiaNote.data; + const instance = await Instance.resolve(authorUrl); + const author = await User.resolve(authorUrl); + + if (!author) { + throw new Error("Entity author could not be resolved"); + } + + const existingNote = await Note.fromSql(eq(Notes.uri, uri.href)); + + const note = + existingNote ?? + (await Note.insert({ + id: randomUUIDv7(), + authorId: author.id, + visibility: "public", + uri: uri.href, + createdAt: new Date(created_at).toISOString(), + })); + + const attachments = await Promise.all( + versiaNote.attachments.map((a) => Media.fromVersia(a)), + ); + + const emojis = await Promise.all( + extensions?.["pub.versia:custom_emojis"]?.emojis.map((emoji) => + Emoji.fetchFromRemote(emoji, instance), + ) ?? [], + ); + + const mentions = ( + await Promise.all( + noteMentions?.map((mention) => User.resolve(mention)) ?? [], + ) + ).filter((m) => m !== null); + + // TODO: Implement groups + const visibility = !group || group instanceof URL ? "direct" : group; + + const reply = replies_to ? await Note.resolve(replies_to) : null; + const quote = quotes ? await Note.resolve(quotes) : null; + const spoiler = subject ? await sanitizedHtmlStrip(subject) : undefined; + + await note.update({ + content: versiaNote.content + ? await contentToHtml(versiaNote.content, mentions) + : undefined, + contentSource: versiaNote.content + ? versiaNote.content.data["text/plain"]?.content || + versiaNote.content.data["text/markdown"]?.content + : undefined, + contentType: "text/html", + visibility: visibility === "followers" ? "private" : visibility, + sensitive: is_sensitive ?? false, + spoilerText: spoiler, + replyId: reply?.id, + quotingId: quote?.id, + }); + + // Emojis, mentions, and attachments are stored in a different table, so update them there too + await note.updateEmojis(emojis); + await note.updateMentions(mentions); + await note.updateAttachments(attachments); + + await note.reload(author.id); + + // Send notifications for mentioned local users + for (const mentioned of mentions) { + if (mentioned.local) { + await mentioned.notify("mention", author, note); } } - let visibility = note.data.group - ? ["public", "followers"].includes(note.data.group as string) - ? (note.data.group as "public" | "private") - : ("url" as const) - : ("direct" as const); - - if (visibility === "url") { - // TODO: Implement groups - visibility = "direct"; - } - - const newData = { - author, - content: - note.content ?? - new VersiaEntities.TextContentFormat({ - "text/plain": { - content: "", - remote: false, - }, - }), - visibility, - isSensitive: note.data.is_sensitive ?? false, - spoilerText: note.data.subject ?? "", - emojis, - uri: note.data.uri, - mentions: ( - await Promise.all( - (note.data.mentions ?? []).map( - async (mention) => await User.resolve(mention), - ), - ) - ).filter((mention) => mention !== null), - mediaAttachments: attachments, - replyId: note.data.replies_to - ? (await Note.resolve(note.data.replies_to))?.data.id - : undefined, - quoteId: note.data.quotes - ? (await Note.resolve(note.data.quotes))?.data.id - : undefined, - }; - - // Check if new note already exists - const foundNote = await Note.fromSql(eq(Notes.uri, note.data.uri.href)); - - // If it exists, simply update it - if (foundNote) { - await foundNote.updateFromData(newData); - - return foundNote; - } - - // Else, create a new note - return await Note.fromData(newData); + return note; } public async delete(ids?: string[]): Promise { diff --git a/classes/database/reaction.ts b/classes/database/reaction.ts index d8825318..87f01529 100644 --- a/classes/database/reaction.ts +++ b/classes/database/reaction.ts @@ -165,7 +165,7 @@ export class Reaction extends BaseInterface { ); } - public isLocal(): boolean { + public get local(): boolean { return this.data.author.instanceId === null; } @@ -174,7 +174,7 @@ export class Reaction extends BaseInterface { } public toVersia(): VersiaEntities.Reaction { - if (!this.isLocal()) { + if (!this.local) { throw new Error("Cannot convert a non-local reaction to Versia"); } @@ -212,7 +212,7 @@ export class Reaction extends BaseInterface { author: User, note: Note, ): Promise { - if (author.isLocal()) { + if (author.local) { throw new Error("Cannot process a reaction from a local user"); } diff --git a/classes/database/user.ts b/classes/database/user.ts index 8b75b612..0268e3d3 100644 --- a/classes/database/user.ts +++ b/classes/database/user.ts @@ -148,12 +148,12 @@ export class User extends BaseInterface { return this.data.id; } - public isLocal(): boolean { + public get local(): boolean { return this.data.instanceId === null; } - public isRemote(): boolean { - return !this.isLocal(); + public get remote(): boolean { + return !this.local; } public get uri(): URL { @@ -196,14 +196,14 @@ export class User extends BaseInterface { ); await foundRelationship.update({ - following: otherUser.isRemote() ? false : !otherUser.data.isLocked, - requested: otherUser.isRemote() ? true : otherUser.data.isLocked, + following: otherUser.remote ? false : !otherUser.data.isLocked, + requested: otherUser.remote ? true : otherUser.data.isLocked, showingReblogs: options?.reblogs, notifying: options?.notify, languages: options?.languages, }); - if (otherUser.isRemote()) { + if (otherUser.remote) { await deliveryQueue.add(DeliveryJobType.FederateEntity, { entity: { type: "Follow", @@ -229,7 +229,7 @@ export class User extends BaseInterface { followee: User, relationship: Relationship, ): Promise { - if (followee.isRemote()) { + if (followee.remote) { await deliveryQueue.add(DeliveryJobType.FederateEntity, { entity: this.unfollowToVersia(followee).toJSON(), recipientId: followee.id, @@ -254,11 +254,11 @@ export class User extends BaseInterface { } public async acceptFollowRequest(follower: User): Promise { - if (!follower.isRemote()) { + if (!follower.remote) { throw new Error("Follower must be a remote user"); } - if (this.isRemote()) { + if (this.remote) { throw new Error("Followee must be a local user"); } @@ -278,11 +278,11 @@ export class User extends BaseInterface { } public async rejectFollowRequest(follower: User): Promise { - if (!follower.isRemote()) { + if (!follower.remote) { throw new Error("Follower must be a remote user"); } - if (this.isRemote()) { + if (this.remote) { throw new Error("Followee must be a local user"); } @@ -497,10 +497,10 @@ export class User extends BaseInterface { uri: uri?.href, }); - if (this.isLocal() && note.author.isLocal()) { + if (this.local && note.author.local) { // Notify the user that their post has been favourited await note.author.notify("favourite", this, note); - } else if (this.isLocal() && note.author.isRemote()) { + } else if (this.local && note.author.remote) { // Federate the like this.federateToFollowers(newLike.toVersia()); } @@ -526,10 +526,10 @@ export class User extends BaseInterface { await likeToDelete.delete(); - if (this.isLocal() && note.author.isLocal()) { + if (this.local && note.author.local) { // Remove any eventual notifications for this like await likeToDelete.clearRelatedNotifications(); - } else if (this.isLocal() && note.author.isRemote()) { + } else if (this.local && note.author.remote) { // User is local, federate the delete this.federateToFollowers(likeToDelete.unlikeToVersia(this)); } @@ -630,7 +630,7 @@ export class User extends BaseInterface { * Takes a Versia User representation, and serializes it to the database. * * If the user already exists, it will update it. - * @param user + * @param versiaUser */ public static async fromVersia( versiaUser: VersiaEntities.User, @@ -895,7 +895,7 @@ export class User extends BaseInterface { } public getAcct(): string { - return this.isLocal() + return this.local ? this.data.username : `${this.data.username}@${this.data.instance?.baseUrl}`; } @@ -921,7 +921,7 @@ export class User extends BaseInterface { // If something important is updated, federate it if ( - this.isLocal() && + this.local && (newUser.username || newUser.displayName || newUser.note || @@ -1090,7 +1090,7 @@ export class User extends BaseInterface { } public toVersia(): VersiaEntities.User { - if (this.isRemote()) { + if (this.remote) { throw new Error("Cannot convert remote user to Versia format"); } diff --git a/classes/functions/status.ts b/classes/functions/status.ts index 42efcf0f..97eba1e7 100644 --- a/classes/functions/status.ts +++ b/classes/functions/status.ts @@ -301,7 +301,7 @@ export const replaceTextMentions = (text: string, mentions: User[]): string => { const linkTemplate = (displayText: string): string => `${displayText}`; - if (mention.isRemote()) { + if (mention.remote) { return finalText.replaceAll( `@${username}@${instance?.baseUrl}`, linkTemplate(`@${username}@${instance?.baseUrl}`), diff --git a/classes/queues/inbox.ts b/classes/queues/inbox.ts index bd549697..2062bb52 100644 --- a/classes/queues/inbox.ts +++ b/classes/queues/inbox.ts @@ -112,7 +112,7 @@ export const getInboxWorker = (): Worker => return; } - if (sender?.isLocal()) { + if (sender?.local) { throw new Error( "Cannot process federation requests from local users", ); diff --git a/cli/user/refetch.ts b/cli/user/refetch.ts index 43d782fc..1eef6b61 100644 --- a/cli/user/refetch.ts +++ b/cli/user/refetch.ts @@ -21,7 +21,7 @@ export const refetchUserCommand = defineCommand( throw new Error(`User ${chalk.gray(handle)} not found.`); } - if (user.isLocal()) { + if (user.local) { throw new Error( "This user is local and as such cannot be refetched.", ); diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 4574e567..7f2e5174 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -458,7 +458,7 @@ export const Notes = pgTable("Notes", { onDelete: "cascade", onUpdate: "cascade", }), - sensitive: boolean("sensitive").notNull(), + sensitive: boolean("sensitive").notNull().default(false), spoilerText: text("spoiler_text").default("").notNull(), applicationId: uuid("applicationId").references(() => Applications.id, { onDelete: "set null",