mirror of
https://github.com/versia-pub/server.git
synced 2025-12-08 09:18:19 +01:00
Add status federation
This commit is contained in:
parent
4acc04cd93
commit
7da7febd00
|
|
@ -160,6 +160,10 @@ export const parseTextMentions = async (text: string) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new status and saves it to the database.
|
||||||
|
* @returns A promise that resolves with the new status.
|
||||||
|
*/
|
||||||
export const createNewStatus = async (
|
export const createNewStatus = async (
|
||||||
author: User,
|
author: User,
|
||||||
content: Lysand.ContentFormat,
|
content: Lysand.ContentFormat,
|
||||||
|
|
@ -253,161 +257,111 @@ export const createNewStatus = async (
|
||||||
return status;
|
return status;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const federateStatus = async (status: StatusWithRelations) => {
|
||||||
* Creates a new status and saves it to the database.
|
const toFederateTo = await getUsersToFederateTo(status);
|
||||||
* @param data The data for the new status.
|
|
||||||
* @returns A promise that resolves with the new status.
|
|
||||||
*/
|
|
||||||
export const createNewStatus2 = async (data: {
|
|
||||||
account: User;
|
|
||||||
application: Application | null;
|
|
||||||
content: string;
|
|
||||||
visibility: APIStatus["visibility"];
|
|
||||||
sensitive: boolean;
|
|
||||||
spoiler_text: string;
|
|
||||||
emojis?: Emoji[];
|
|
||||||
content_type?: string;
|
|
||||||
uri?: string;
|
|
||||||
mentions?: UserWithRelations[];
|
|
||||||
media_attachments?: string[];
|
|
||||||
reply?: {
|
|
||||||
status: Status;
|
|
||||||
user: User;
|
|
||||||
};
|
|
||||||
quote?: Status;
|
|
||||||
}) => {
|
|
||||||
// Get people mentioned in the content (match @username or @username@domain.com mentions)
|
|
||||||
const mentionedPeople =
|
|
||||||
data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? [];
|
|
||||||
|
|
||||||
let mentions = data.mentions || [];
|
for (const user of toFederateTo) {
|
||||||
|
// TODO: Add queue system
|
||||||
|
const request = await statusToInboxRequest(status, user);
|
||||||
|
|
||||||
// Parse emojis
|
// Send request
|
||||||
const emojis = await parseEmojis(data.content);
|
const response = await fetch(request);
|
||||||
|
|
||||||
data.emojis = data.emojis ? [...data.emojis, ...emojis] : emojis;
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to federate status ${status.id} to ${user.uri}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Get list of mentioned users
|
export const statusToInboxRequest = async (
|
||||||
if (mentions.length === 0) {
|
status: StatusWithRelations,
|
||||||
mentions = await client.user.findMany({
|
user: User,
|
||||||
|
): Promise<Request> => {
|
||||||
|
const output = statusToLysand(status);
|
||||||
|
|
||||||
|
if (!user.instanceId || !user.endpoints.inbox) {
|
||||||
|
throw new Error("User has no inbox or is a local user");
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateKey = await crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
Uint8Array.from(atob(status.author.privateKey ?? ""), (c) =>
|
||||||
|
c.charCodeAt(0),
|
||||||
|
),
|
||||||
|
"Ed25519",
|
||||||
|
false,
|
||||||
|
["sign"],
|
||||||
|
);
|
||||||
|
|
||||||
|
const digest = await crypto.subtle.digest(
|
||||||
|
"SHA-256",
|
||||||
|
new TextEncoder().encode(JSON.stringify(output)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const userInbox = new URL(user.endpoints.inbox);
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
const signature = await crypto.subtle.sign(
|
||||||
|
"Ed25519",
|
||||||
|
privateKey,
|
||||||
|
new TextEncoder().encode(
|
||||||
|
`(request-target): post ${userInbox.pathname}\n` +
|
||||||
|
`host: ${userInbox.host}\n` +
|
||||||
|
`date: ${date.toISOString()}\n` +
|
||||||
|
`digest: SHA-256=${btoa(
|
||||||
|
String.fromCharCode(...new Uint8Array(digest)),
|
||||||
|
)}\n`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const signatureBase64 = btoa(
|
||||||
|
String.fromCharCode(...new Uint8Array(signature)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Request(userInbox, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Date: date.toISOString(),
|
||||||
|
Origin: config.http.base_url,
|
||||||
|
Signature: `keyId="${status.author.uri}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(output),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUsersToFederateTo = async (status: StatusWithRelations) => {
|
||||||
|
return await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: mentionedPeople.map((person) => ({
|
OR: [
|
||||||
username: person.split("@")[1],
|
["public", "unlisted", "private"].includes(status.visibility)
|
||||||
instance: {
|
|
||||||
base_url: person.split("@")[2],
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
include: userRelations,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let formattedContent = "";
|
|
||||||
|
|
||||||
// Get HTML version of content
|
|
||||||
if (data.content_type === "text/markdown") {
|
|
||||||
formattedContent = linkifyHtml(
|
|
||||||
await sanitizeHtml(await parse(data.content)),
|
|
||||||
);
|
|
||||||
} else if (data.content_type === "text/x.misskeymarkdown") {
|
|
||||||
// Parse as MFM
|
|
||||||
} else {
|
|
||||||
// Parse as plaintext
|
|
||||||
formattedContent = linkifyStr(data.content);
|
|
||||||
|
|
||||||
// Split by newline and add <p> tags
|
|
||||||
formattedContent = formattedContent
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => `<p>${line}</p>`)
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Turn each @username or @username@instance mention into an anchor link
|
|
||||||
for (const mention of mentions) {
|
|
||||||
const matches = data.content.match(
|
|
||||||
new RegExp(
|
|
||||||
`@${mention.username}(@${mention.instance?.base_url})?`,
|
|
||||||
"g",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!matches) continue;
|
|
||||||
|
|
||||||
for (const match of matches) {
|
|
||||||
formattedContent = formattedContent.replace(
|
|
||||||
new RegExp(
|
|
||||||
`@${mention.username}(@${mention.instance?.base_url})?`,
|
|
||||||
"g",
|
|
||||||
),
|
|
||||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${
|
|
||||||
mention.uri ||
|
|
||||||
new URL(
|
|
||||||
`/@${mention.username}`,
|
|
||||||
config.http.base_url,
|
|
||||||
).toString()
|
|
||||||
}">${match}</a>`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = await client.status.create({
|
|
||||||
data: {
|
|
||||||
authorId: data.account.id,
|
|
||||||
applicationId: data.application?.id,
|
|
||||||
content: formattedContent,
|
|
||||||
contentSource: data.content,
|
|
||||||
contentType: data.content_type,
|
|
||||||
visibility: data.visibility,
|
|
||||||
sensitive: data.sensitive,
|
|
||||||
spoilerText: data.spoiler_text,
|
|
||||||
emojis: {
|
|
||||||
connect: data.emojis.map((emoji) => {
|
|
||||||
return {
|
|
||||||
id: emoji.id,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
attachments: data.media_attachments
|
|
||||||
? {
|
? {
|
||||||
connect: data.media_attachments.map((attachment) => {
|
relationships: {
|
||||||
return {
|
some: {
|
||||||
id: attachment,
|
subjectId: status.authorId,
|
||||||
};
|
following: true,
|
||||||
}),
|
},
|
||||||
|
},
|
||||||
|
instanceId: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: {},
|
||||||
inReplyToPostId: data.reply?.status.id,
|
// Mentioned users
|
||||||
quotingPostId: data.quote?.id,
|
{
|
||||||
instanceId: data.account.instanceId || undefined,
|
id: {
|
||||||
isReblog: false,
|
in: status.mentions.map((m) => m.id),
|
||||||
uri: data.uri || null,
|
},
|
||||||
mentions: {
|
instanceId: {
|
||||||
connect: mentions.map((mention) => {
|
not: null,
|
||||||
return {
|
|
||||||
id: mention.id,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
],
|
||||||
});
|
|
||||||
|
|
||||||
// Create notification
|
|
||||||
if (status.inReplyToPost) {
|
|
||||||
await client.notification.create({
|
|
||||||
data: {
|
|
||||||
notifiedId: status.inReplyToPost.authorId,
|
|
||||||
accountId: status.authorId,
|
|
||||||
type: "mention",
|
|
||||||
statusId: status.id,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Add to search index
|
|
||||||
await addStausToMeilisearch(status);
|
|
||||||
|
|
||||||
return status;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editStatus = async (
|
export const editStatus = async (
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,11 @@ import { parse } from "marked";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
import { getFromToken } from "~database/entities/Application";
|
import { getFromToken } from "~database/entities/Application";
|
||||||
import type { StatusWithRelations } from "~database/entities/Status";
|
import type { StatusWithRelations } from "~database/entities/Status";
|
||||||
import { createNewStatus, statusToAPI } from "~database/entities/Status";
|
import {
|
||||||
|
createNewStatus,
|
||||||
|
federateStatus,
|
||||||
|
statusToAPI,
|
||||||
|
} from "~database/entities/Status";
|
||||||
import type { UserWithRelations } from "~database/entities/User";
|
import type { UserWithRelations } from "~database/entities/User";
|
||||||
import { statusAndUserRelations } from "~database/entities/relations";
|
import { statusAndUserRelations } from "~database/entities/relations";
|
||||||
import type { APIStatus } from "~types/entities/status";
|
import type { APIStatus } from "~types/entities/status";
|
||||||
|
|
@ -233,7 +237,7 @@ export default apiRoute<{
|
||||||
quote ?? undefined,
|
quote ?? undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: add database jobs to deliver the post
|
await federateStatus(newStatus);
|
||||||
|
|
||||||
return jsonResponse(await statusToAPI(newStatus, user));
|
return jsonResponse(await statusToAPI(newStatus, user));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue