mirror of
https://github.com/versia-pub/server.git
synced 2025-12-07 16:58:20 +01:00
refactor(federation): 🔥 Refactor Note federation and creation code
This commit is contained in:
parent
54b2dfb78d
commit
f79b0bc999
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
const sanitizedSpoilerText = spoiler_text
|
||||
? await sanitizedHtmlStrip(spoiler_text)
|
||||
: undefined;
|
||||
|
||||
const content = status
|
||||
? new VersiaEntities.TextContentFormat({
|
||||
[content_type]: {
|
||||
content: status ?? "",
|
||||
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);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof Notes, NoteTypeWithRelations> {
|
|||
);
|
||||
}
|
||||
|
||||
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<Note> {
|
||||
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<typeof StatusSchema.shape.visibility>;
|
||||
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<Note> {
|
||||
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<typeof StatusSchema.shape.visibility>;
|
||||
isSensitive?: boolean;
|
||||
spoilerText?: string;
|
||||
emojis?: Emoji[];
|
||||
uri?: URL;
|
||||
mentions?: User[];
|
||||
mediaAttachments?: Media[];
|
||||
replyId?: string;
|
||||
quoteId?: string;
|
||||
application?: Application;
|
||||
}): Promise<Note> {
|
||||
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<typeof Notes, NoteTypeWithRelations> {
|
|||
*/
|
||||
public static async resolve(uri: URL): Promise<Note | null> {
|
||||
// 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<typeof Notes, NoteTypeWithRelations> {
|
|||
return await Note.fromId(uuid[0]);
|
||||
}
|
||||
|
||||
return Note.fromVersia(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to fetch a Versia Note from the given URL.
|
||||
*
|
||||
* @param url The URL to fetch the note from
|
||||
*/
|
||||
public static async fromVersia(url: URL): Promise<Note>;
|
||||
|
||||
/**
|
||||
* 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<Note>;
|
||||
|
||||
public static async fromVersia(
|
||||
versiaNote: VersiaEntities.Note | URL,
|
||||
): Promise<Note> {
|
||||
if (versiaNote instanceof URL) {
|
||||
// No bridge support for notes yet
|
||||
const note = await User.federationRequester.fetchEntity(
|
||||
uri,
|
||||
versiaNote,
|
||||
VersiaEntities.Note,
|
||||
);
|
||||
|
||||
return Note.fromVersia(note);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static async fromVersia(note: VersiaEntities.Note): Promise<Note> {
|
||||
const emojis: Emoji[] = [];
|
||||
const logger = getLogger(["federation", "resolvers"]);
|
||||
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);
|
||||
|
||||
const author = await User.resolve(note.data.author);
|
||||
if (!author) {
|
||||
throw new Error("Invalid object author");
|
||||
throw new Error("Entity author could not be resolved");
|
||||
}
|
||||
|
||||
const instance = await Instance.resolve(note.data.uri);
|
||||
const existingNote = await Note.fromSql(eq(Notes.uri, uri.href));
|
||||
|
||||
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;
|
||||
});
|
||||
const note =
|
||||
existingNote ??
|
||||
(await Note.insert({
|
||||
id: randomUUIDv7(),
|
||||
authorId: author.id,
|
||||
visibility: "public",
|
||||
uri: uri.href,
|
||||
createdAt: new Date(created_at).toISOString(),
|
||||
}));
|
||||
|
||||
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;
|
||||
},
|
||||
const attachments = await Promise.all(
|
||||
versiaNote.attachments.map((a) => Media.fromVersia(a)),
|
||||
);
|
||||
|
||||
if (resolvedAttachment) {
|
||||
attachments.push(resolvedAttachment);
|
||||
}
|
||||
}
|
||||
const emojis = await Promise.all(
|
||||
extensions?.["pub.versia:custom_emojis"]?.emojis.map((emoji) =>
|
||||
Emoji.fetchFromRemote(emoji, instance),
|
||||
) ?? [],
|
||||
);
|
||||
|
||||
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: (
|
||||
const mentions = (
|
||||
await Promise.all(
|
||||
(note.data.mentions ?? []).map(
|
||||
async (mention) => await User.resolve(mention),
|
||||
),
|
||||
noteMentions?.map((mention) => User.resolve(mention)) ?? [],
|
||||
)
|
||||
).filter((mention) => mention !== null),
|
||||
mediaAttachments: attachments,
|
||||
replyId: note.data.replies_to
|
||||
? (await Note.resolve(note.data.replies_to))?.data.id
|
||||
).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,
|
||||
quoteId: note.data.quotes
|
||||
? (await Note.resolve(note.data.quotes))?.data.id
|
||||
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,
|
||||
});
|
||||
|
||||
// Check if new note already exists
|
||||
const foundNote = await Note.fromSql(eq(Notes.uri, note.data.uri.href));
|
||||
// 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);
|
||||
|
||||
// If it exists, simply update it
|
||||
if (foundNote) {
|
||||
await foundNote.updateFromData(newData);
|
||||
await note.reload(author.id);
|
||||
|
||||
return foundNote;
|
||||
// Send notifications for mentioned local users
|
||||
for (const mentioned of mentions) {
|
||||
if (mentioned.local) {
|
||||
await mentioned.notify("mention", author, note);
|
||||
}
|
||||
}
|
||||
|
||||
// Else, create a new note
|
||||
return await Note.fromData(newData);
|
||||
return note;
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
|
|||
);
|
||||
}
|
||||
|
||||
public isLocal(): boolean {
|
||||
public get local(): boolean {
|
||||
return this.data.author.instanceId === null;
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +174,7 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
|
|||
}
|
||||
|
||||
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<typeof Reactions, ReactionType> {
|
|||
author: User,
|
||||
note: Note,
|
||||
): Promise<Reaction> {
|
||||
if (author.isLocal()) {
|
||||
if (author.local) {
|
||||
throw new Error("Cannot process a reaction from a local user");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,12 +148,12 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
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<typeof Users, UserWithRelations> {
|
|||
);
|
||||
|
||||
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<typeof Users, UserWithRelations> {
|
|||
followee: User,
|
||||
relationship: Relationship,
|
||||
): Promise<void> {
|
||||
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<typeof Users, UserWithRelations> {
|
|||
}
|
||||
|
||||
public async acceptFollowRequest(follower: User): Promise<void> {
|
||||
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<typeof Users, UserWithRelations> {
|
|||
}
|
||||
|
||||
public async rejectFollowRequest(follower: User): Promise<void> {
|
||||
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<typeof Users, UserWithRelations> {
|
|||
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<typeof Users, UserWithRelations> {
|
|||
|
||||
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<typeof Users, UserWithRelations> {
|
|||
* 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<typeof Users, UserWithRelations> {
|
|||
}
|
||||
|
||||
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<typeof Users, UserWithRelations> {
|
|||
|
||||
// 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<typeof Users, UserWithRelations> {
|
|||
}
|
||||
|
||||
public toVersia(): VersiaEntities.User {
|
||||
if (this.isRemote()) {
|
||||
if (this.remote) {
|
||||
throw new Error("Cannot convert remote user to Versia format");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ export const replaceTextMentions = (text: string, mentions: User[]): string => {
|
|||
const linkTemplate = (displayText: string): string =>
|
||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${uri}">${displayText}</a>`;
|
||||
|
||||
if (mention.isRemote()) {
|
||||
if (mention.remote) {
|
||||
return finalText.replaceAll(
|
||||
`@${username}@${instance?.baseUrl}`,
|
||||
linkTemplate(`@${username}@${instance?.baseUrl}`),
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export const getInboxWorker = (): Worker<InboxJobData, void, InboxJobType> =>
|
|||
return;
|
||||
}
|
||||
|
||||
if (sender?.isLocal()) {
|
||||
if (sender?.local) {
|
||||
throw new Error(
|
||||
"Cannot process federation requests from local users",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue