mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
refactor(federation): ♻️ More federation logic cleanup
This commit is contained in:
parent
83399ba5f1
commit
0ae9cfe26c
|
|
@ -117,6 +117,10 @@ export default apiRoute((app) =>
|
|||
|
||||
const uri = await User.webFinger(manager, username, domain);
|
||||
|
||||
if (!uri) {
|
||||
return context.json({ error: "Account not found" }, 404);
|
||||
}
|
||||
|
||||
const foundAccount = await User.resolve(uri);
|
||||
|
||||
if (foundAccount) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import { apiRoute, applyConfig, auth, userAddressValidator } from "@/api";
|
||||
import {
|
||||
apiRoute,
|
||||
applyConfig,
|
||||
auth,
|
||||
parseUserAddress,
|
||||
userAddressValidator,
|
||||
} from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||
|
|
@ -77,19 +83,21 @@ export default apiRoute((app) =>
|
|||
return context.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const [username, host] = q.replace(/^@/, "").split("@");
|
||||
const { username, domain } = parseUserAddress(q);
|
||||
|
||||
const accounts: User[] = [];
|
||||
|
||||
if (resolve && username && host) {
|
||||
if (resolve && domain) {
|
||||
const manager = await (self ?? User).getFederationRequester();
|
||||
|
||||
const uri = await User.webFinger(manager, username, host);
|
||||
const uri = await User.webFinger(manager, username, domain);
|
||||
|
||||
const resolvedUser = await User.resolve(uri);
|
||||
if (uri) {
|
||||
const resolvedUser = await User.resolve(uri);
|
||||
|
||||
if (resolvedUser) {
|
||||
accounts.push(resolvedUser);
|
||||
if (resolvedUser) {
|
||||
accounts.push(resolvedUser);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
accounts.push(
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export default apiRoute((app) =>
|
|||
}
|
||||
|
||||
if (note.author.isLocal() && user.isLocal()) {
|
||||
await note.author.createNotification("reblog", user, newReblog);
|
||||
await note.author.notify("reblog", user, newReblog);
|
||||
}
|
||||
|
||||
return context.json(await finalNewReblog.toApi(user), 201);
|
||||
|
|
|
|||
|
|
@ -163,17 +163,19 @@ export default apiRoute((app) =>
|
|||
|
||||
const uri = await User.webFinger(manager, username, domain);
|
||||
|
||||
const newUser = await User.resolve(uri);
|
||||
if (uri) {
|
||||
const newUser = await User.resolve(uri);
|
||||
|
||||
if (newUser) {
|
||||
return context.json(
|
||||
{
|
||||
accounts: [newUser.toApi()],
|
||||
statuses: [],
|
||||
hashtags: [],
|
||||
},
|
||||
200,
|
||||
);
|
||||
if (newUser) {
|
||||
return context.json(
|
||||
{
|
||||
accounts: [newUser.toApi()],
|
||||
statuses: [],
|
||||
hashtags: [],
|
||||
},
|
||||
200,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { proxyUrl } from "@/response";
|
|||
import type { Emoji as APIEmoji } from "@versia/client/types";
|
||||
import type { CustomEmojiExtension } from "@versia/federation/types";
|
||||
import { type Instance, db } from "@versia/kit/db";
|
||||
import { Emojis, Instances } from "@versia/kit/tables";
|
||||
import { Emojis, type Instances } from "@versia/kit/tables";
|
||||
import {
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
|
|
@ -137,26 +137,15 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiWithInstance> {
|
|||
emojiToFetch: CustomEmojiExtension["emojis"][0],
|
||||
instance: Instance,
|
||||
): Promise<Emoji> {
|
||||
const existingEmoji = await db
|
||||
.select()
|
||||
.from(Emojis)
|
||||
.innerJoin(Instances, eq(Emojis.instanceId, Instances.id))
|
||||
.where(
|
||||
and(
|
||||
eq(Emojis.shortcode, emojiToFetch.name),
|
||||
eq(Instances.id, instance.id),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
const existingEmoji = await Emoji.fromSql(
|
||||
and(
|
||||
eq(Emojis.shortcode, emojiToFetch.name),
|
||||
eq(Emojis.instanceId, instance.id),
|
||||
),
|
||||
);
|
||||
|
||||
if (existingEmoji[0]) {
|
||||
const found = await Emoji.fromId(existingEmoji[0].Emojis.id);
|
||||
|
||||
if (!found) {
|
||||
throw new Error("Failed to fetch emoji");
|
||||
}
|
||||
|
||||
return found;
|
||||
if (existingEmoji) {
|
||||
return existingEmoji;
|
||||
}
|
||||
|
||||
return await Emoji.fromVersia(emojiToFetch, instance);
|
||||
|
|
|
|||
|
|
@ -492,11 +492,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
|||
// Send notifications for mentioned local users
|
||||
for (const mention of parsedMentions) {
|
||||
if (mention.isLocal()) {
|
||||
await mention.createNotification(
|
||||
"mention",
|
||||
data.author,
|
||||
newNote,
|
||||
);
|
||||
await mention.notify("mention", data.author, newNote);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
senderId: this.id,
|
||||
});
|
||||
} else {
|
||||
await otherUser.createNotification(
|
||||
await otherUser.notify(
|
||||
otherUser.data.isLocked ? "follow_request" : "follow",
|
||||
this,
|
||||
);
|
||||
|
|
@ -362,19 +362,31 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
});
|
||||
}
|
||||
|
||||
public static async webFinger(
|
||||
/**
|
||||
* Perform a WebFinger lookup to find a user's URI
|
||||
* @param manager
|
||||
* @param username
|
||||
* @param hostname
|
||||
* @returns URI, or null if not found
|
||||
*/
|
||||
public static webFinger(
|
||||
manager: FederationRequester,
|
||||
username: string,
|
||||
hostname: string,
|
||||
): Promise<string> {
|
||||
return (
|
||||
(await manager.webFinger(username, hostname).catch(() => null)) ??
|
||||
(await manager.webFinger(
|
||||
username,
|
||||
hostname,
|
||||
"application/activity+json",
|
||||
))
|
||||
);
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
return manager.webFinger(username, hostname);
|
||||
} catch {
|
||||
try {
|
||||
return manager.webFinger(
|
||||
username,
|
||||
hostname,
|
||||
"application/activity+json",
|
||||
);
|
||||
} catch {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static getCount(): Promise<number> {
|
||||
|
|
@ -511,7 +523,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
|
||||
if (this.isLocal() && note.author.isLocal()) {
|
||||
// Notify the user that their post has been favourited
|
||||
await note.author.createNotification("favourite", this, note);
|
||||
await note.author.notify("favourite", this, note);
|
||||
} else if (this.isLocal() && note.author.isRemote()) {
|
||||
// Federate the like
|
||||
this.federateToFollowers(newLike.toVersia());
|
||||
|
|
@ -547,7 +559,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
}
|
||||
}
|
||||
|
||||
public async createNotification(
|
||||
public async notify(
|
||||
type: "mention" | "follow_request" | "follow" | "favourite" | "reblog",
|
||||
relatedUser: User,
|
||||
note?: Note,
|
||||
|
|
|
|||
|
|
@ -218,9 +218,9 @@ export const parseTextMentions = async (
|
|||
}
|
||||
|
||||
const baseUrlHost = new URL(config.http.base_url).host;
|
||||
|
||||
const isLocal = (host?: string): boolean => host === baseUrlHost || !host;
|
||||
|
||||
// Find local and matching users
|
||||
const foundUsers = await db
|
||||
.select({
|
||||
id: Users.id,
|
||||
|
|
@ -233,47 +233,46 @@ export const parseTextMentions = async (
|
|||
or(
|
||||
...mentionedPeople.map((person) =>
|
||||
and(
|
||||
eq(Users.username, person?.[1] ?? ""),
|
||||
isLocal(person?.[2])
|
||||
eq(Users.username, person[1] ?? ""),
|
||||
isLocal(person[2])
|
||||
? isNull(Users.instanceId)
|
||||
: eq(Instances.baseUrl, person?.[2] ?? ""),
|
||||
: eq(Instances.baseUrl, person[2] ?? ""),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Separate found and unresolved users
|
||||
const finalList = await User.manyFromSql(
|
||||
inArray(
|
||||
Users.id,
|
||||
foundUsers.map((u) => u.id),
|
||||
),
|
||||
);
|
||||
|
||||
// Every remote user that isn't in database
|
||||
const notFoundRemoteUsers = mentionedPeople.filter(
|
||||
(person) =>
|
||||
(p) =>
|
||||
!(
|
||||
isLocal(person?.[2]) ||
|
||||
foundUsers.find(
|
||||
(user) =>
|
||||
user.username === person?.[1] &&
|
||||
user.baseUrl === person?.[2],
|
||||
)
|
||||
foundUsers.some(
|
||||
(user) => user.username === p[1] && user.baseUrl === p[2],
|
||||
) || isLocal(p[2])
|
||||
),
|
||||
);
|
||||
|
||||
const finalList =
|
||||
foundUsers.length > 0
|
||||
? await User.manyFromSql(
|
||||
inArray(
|
||||
Users.id,
|
||||
foundUsers.map((u) => u.id),
|
||||
),
|
||||
)
|
||||
: [];
|
||||
|
||||
// Attempt to resolve mentions that were not found
|
||||
// Resolve remote mentions not in database
|
||||
for (const person of notFoundRemoteUsers) {
|
||||
const manager = await author.getFederationRequester();
|
||||
|
||||
const uri = await User.webFinger(
|
||||
manager,
|
||||
person?.[1] ?? "",
|
||||
person?.[2] ?? "",
|
||||
person[1] ?? "",
|
||||
person[2] ?? "",
|
||||
);
|
||||
|
||||
if (!uri) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const user = await User.resolve(uri);
|
||||
|
||||
if (user) {
|
||||
|
|
@ -285,51 +284,35 @@ export const parseTextMentions = async (
|
|||
};
|
||||
|
||||
export const replaceTextMentions = (text: string, mentions: User[]): string => {
|
||||
let finalText = text;
|
||||
for (const mention of mentions) {
|
||||
const user = mention.data;
|
||||
// Replace @username and @username@domain
|
||||
if (user.instance) {
|
||||
finalText = finalText.replace(
|
||||
createRegExp(
|
||||
exactly(`@${user.username}@${user.instance.baseUrl}`),
|
||||
[global],
|
||||
),
|
||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${mention.getUri()}">@${
|
||||
user.username
|
||||
}@${user.instance.baseUrl}</a>`,
|
||||
return mentions.reduce((finalText, mention) => {
|
||||
const { username, instance } = mention.data;
|
||||
const uri = mention.getUri();
|
||||
const baseHost = new URL(config.http.base_url).host;
|
||||
const linkTemplate = (displayText: string): string =>
|
||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${uri}">${displayText}</a>`;
|
||||
|
||||
if (mention.isRemote()) {
|
||||
return finalText.replaceAll(
|
||||
`@${username}@${instance?.baseUrl}`,
|
||||
linkTemplate(`@${username}@${instance?.baseUrl}`),
|
||||
);
|
||||
} else {
|
||||
finalText = finalText.replace(
|
||||
// Only replace @username if it doesn't have another @ right after
|
||||
}
|
||||
|
||||
return finalText
|
||||
.replace(
|
||||
createRegExp(
|
||||
exactly(`@${user.username}`)
|
||||
exactly(`@${username}`)
|
||||
.notBefore(anyOf(letter, digit, charIn("@")))
|
||||
.notAfter(anyOf(letter, digit, charIn("@"))),
|
||||
[global],
|
||||
),
|
||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${mention.getUri()}">@${
|
||||
user.username
|
||||
}</a>`,
|
||||
linkTemplate(`@${username}@${baseHost}`),
|
||||
)
|
||||
.replaceAll(
|
||||
`@${username}@${baseHost}`,
|
||||
linkTemplate(`@${username}@${baseHost}`),
|
||||
);
|
||||
|
||||
finalText = finalText.replace(
|
||||
createRegExp(
|
||||
exactly(
|
||||
`@${user.username}@${
|
||||
new URL(config.http.base_url).host
|
||||
}`,
|
||||
),
|
||||
[global],
|
||||
),
|
||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${mention.getUri()}">@${
|
||||
user.username
|
||||
}</a>`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return finalText;
|
||||
}, text);
|
||||
};
|
||||
|
||||
export const contentToHtml = async (
|
||||
|
|
@ -337,8 +320,8 @@ export const contentToHtml = async (
|
|||
mentions: User[] = [],
|
||||
inline = false,
|
||||
): Promise<string> => {
|
||||
let htmlContent: string;
|
||||
const sanitizer = inline ? sanitizeHtmlInline : sanitizeHtml;
|
||||
let htmlContent = "";
|
||||
|
||||
if (content["text/html"]) {
|
||||
htmlContent = await sanitizer(content["text/html"].content);
|
||||
|
|
@ -347,29 +330,20 @@ export const contentToHtml = async (
|
|||
await markdownParse(content["text/markdown"].content),
|
||||
);
|
||||
} else if (content["text/plain"]?.content) {
|
||||
// Split by newline and add <p> tags
|
||||
htmlContent = (await sanitizer(content["text/plain"].content))
|
||||
.split("\n")
|
||||
.map((line) => `<p>${line}</p>`)
|
||||
.join("\n");
|
||||
} else {
|
||||
htmlContent = "";
|
||||
}
|
||||
|
||||
// Replace mentions text
|
||||
htmlContent = await replaceTextMentions(htmlContent, mentions ?? []);
|
||||
htmlContent = replaceTextMentions(htmlContent, mentions);
|
||||
|
||||
// Linkify
|
||||
htmlContent = linkifyHtml(htmlContent, {
|
||||
return linkifyHtml(htmlContent, {
|
||||
defaultProtocol: "https",
|
||||
validate: {
|
||||
email: (): false => false,
|
||||
},
|
||||
validate: { email: (): false => false },
|
||||
target: "_blank",
|
||||
rel: "nofollow noopener noreferrer",
|
||||
});
|
||||
|
||||
return htmlContent;
|
||||
};
|
||||
|
||||
export const markdownParse = async (content: string): Promise<string> => {
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ export class InboxProcessor {
|
|||
languages: [],
|
||||
});
|
||||
|
||||
await followee.createNotification(
|
||||
await followee.notify(
|
||||
followee.data.isLocked ? "follow_request" : "follow",
|
||||
author,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@ export default class FederationUserFetch extends BaseCommand<
|
|||
|
||||
const uri = await User.webFinger(manager, username, host);
|
||||
|
||||
if (!uri) {
|
||||
spinner.fail();
|
||||
this.log(chalk.red("User not found"));
|
||||
this.exit(1);
|
||||
}
|
||||
|
||||
const newUser = await User.resolve(uri);
|
||||
|
||||
if (newUser) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue