mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
refactor(federation): 🔥 Remove old code and simplify federation requests
This commit is contained in:
parent
ad9ed2598c
commit
2f823317c2
18 changed files with 182 additions and 302 deletions
|
|
@ -1,20 +1,25 @@
|
|||
import { emojiValidatorWithColons } from "@/api";
|
||||
import { proxyUrl } from "@/response";
|
||||
import type { Emoji as ApiEmoji } from "@lysand-org/client/types";
|
||||
import type { CustomEmojiExtension } from "@lysand-org/federation/types";
|
||||
import {
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
type SQL,
|
||||
and,
|
||||
desc,
|
||||
eq,
|
||||
inArray,
|
||||
} from "drizzle-orm";
|
||||
import type { EmojiWithInstance } from "~/classes/functions/emoji";
|
||||
import { db } from "~/drizzle/db";
|
||||
import { Emojis, Instances } from "~/drizzle/schema";
|
||||
import { BaseInterface } from "./base";
|
||||
import { Instance } from "./instance";
|
||||
|
||||
export type EmojiWithInstance = InferSelectModel<typeof Emojis> & {
|
||||
instance: InferSelectModel<typeof Instances> | null;
|
||||
};
|
||||
|
||||
export class Emoji extends BaseInterface<typeof Emojis, EmojiWithInstance> {
|
||||
async reload(): Promise<void> {
|
||||
const reloaded = await Emoji.fromId(this.data.id);
|
||||
|
|
@ -152,6 +157,26 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiWithInstance> {
|
|||
return this.data.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse emojis from text
|
||||
*
|
||||
* @param text The text to parse
|
||||
* @returns An array of emojis
|
||||
*/
|
||||
public static async parseFromText(text: string): Promise<Emoji[]> {
|
||||
const matches = text.match(emojiValidatorWithColons);
|
||||
if (!matches || matches.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Emoji.manyFromSql(
|
||||
inArray(
|
||||
Emojis.shortcode,
|
||||
matches.map((match) => match.replace(/:/g, "")),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public toApi(): ApiEmoji {
|
||||
return {
|
||||
// @ts-expect-error ID is not in regular Mastodon API
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { getLogger } from "@logtape/logtape";
|
||||
import {
|
||||
EntityValidator,
|
||||
FederationRequester,
|
||||
type ResponseError,
|
||||
type ValidationError,
|
||||
} from "@lysand-org/federation";
|
||||
|
|
@ -20,6 +19,7 @@ import {
|
|||
import { db } from "~/drizzle/db";
|
||||
import { Instances } from "~/drizzle/schema";
|
||||
import { BaseInterface } from "./base";
|
||||
import { User } from "./user";
|
||||
|
||||
export type AttachmentType = InferSelectModel<typeof Instances>;
|
||||
|
||||
|
|
@ -139,8 +139,10 @@ export class Instance extends BaseInterface<typeof Instances> {
|
|||
const wellKnownUrl = new URL("/.well-known/lysand", origin);
|
||||
const logger = getLogger("federation");
|
||||
|
||||
const requester = await User.getServerActor().getFederationRequester();
|
||||
|
||||
try {
|
||||
const { ok, raw, data } = await new FederationRequester()
|
||||
const { ok, raw, data } = await requester
|
||||
.get(wellKnownUrl, {
|
||||
// @ts-expect-error Bun extension
|
||||
proxy: config.http.proxy.address,
|
||||
|
|
@ -193,13 +195,14 @@ export class Instance extends BaseInterface<typeof Instances> {
|
|||
// Go to endpoint, then follow the links to the actual metadata
|
||||
|
||||
const logger = getLogger("federation");
|
||||
const requester = await User.getServerActor().getFederationRequester();
|
||||
|
||||
try {
|
||||
const {
|
||||
raw: response,
|
||||
ok,
|
||||
data: wellKnown,
|
||||
} = await new FederationRequester()
|
||||
} = await requester
|
||||
.get<{
|
||||
links: { rel: string; href: string }[];
|
||||
}>(wellKnownUrl, {
|
||||
|
|
@ -245,7 +248,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
|||
raw: metadataResponse,
|
||||
ok: ok2,
|
||||
data: metadata,
|
||||
} = await new FederationRequester()
|
||||
} = await requester
|
||||
.get<{
|
||||
metadata: {
|
||||
nodeName?: string;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type {
|
|||
Attachment as ApiAttachment,
|
||||
Status as ApiStatus,
|
||||
} from "@lysand-org/client/types";
|
||||
import { EntityValidator, FederationRequester } from "@lysand-org/federation";
|
||||
import { EntityValidator } from "@lysand-org/federation";
|
||||
import type {
|
||||
ContentFormat,
|
||||
Note as LysandNote,
|
||||
|
|
@ -29,7 +29,6 @@ import {
|
|||
type Application,
|
||||
applicationToApi,
|
||||
} from "~/classes/functions/application";
|
||||
import { parseEmojis } from "~/classes/functions/emoji";
|
||||
import { localObjectUri } from "~/classes/functions/federation";
|
||||
import {
|
||||
type StatusWithRelations,
|
||||
|
|
@ -190,6 +189,14 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
|||
return this.data.id;
|
||||
}
|
||||
|
||||
async federateToUsers(): Promise<void> {
|
||||
const users = await this.getUsersToFederateTo();
|
||||
|
||||
for (const user of users) {
|
||||
await this.author.federateToUser(this.toLysand(), user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the users that should be federated to for this note
|
||||
*
|
||||
|
|
@ -336,7 +343,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
|||
|
||||
const parsedEmojis = [
|
||||
...(data.emojis ?? []),
|
||||
...(await parseEmojis(plaintextContent)),
|
||||
...(await Emoji.parseFromText(plaintextContent)),
|
||||
// Deduplicate by .id
|
||||
].filter(
|
||||
(emoji, index, self) =>
|
||||
|
|
@ -429,7 +436,9 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
|||
|
||||
const parsedEmojis = [
|
||||
...(data.emojis ?? []),
|
||||
...(plaintextContent ? await parseEmojis(plaintextContent) : []),
|
||||
...(plaintextContent
|
||||
? await Emoji.parseFromText(plaintextContent)
|
||||
: []),
|
||||
// Deduplicate by .id
|
||||
].filter(
|
||||
(emoji, index, self) =>
|
||||
|
|
@ -592,7 +601,10 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
|||
throw new Error(`Invalid URI to parse ${uri}`);
|
||||
}
|
||||
|
||||
const { data } = await new FederationRequester().get(uri, {
|
||||
const requester =
|
||||
await User.getServerActor().getFederationRequester();
|
||||
|
||||
const { data } = await requester.get(uri, {
|
||||
// @ts-expect-error Bun extension
|
||||
proxy: config.http.proxy.address,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,12 +2,20 @@ import { idValidator } from "@/api";
|
|||
import { getBestContentType, urlToContentFormat } from "@/content_types";
|
||||
import { randomString } from "@/math";
|
||||
import { proxyUrl } from "@/response";
|
||||
import { sentry } from "@/sentry";
|
||||
import { getLogger } from "@logtape/logtape";
|
||||
import type {
|
||||
Account as ApiAccount,
|
||||
Mention as ApiMention,
|
||||
} from "@lysand-org/client/types";
|
||||
import { EntityValidator, FederationRequester } from "@lysand-org/federation";
|
||||
import {
|
||||
EntityValidator,
|
||||
FederationRequester,
|
||||
type HttpVerb,
|
||||
SignatureConstructor,
|
||||
} from "@lysand-org/federation";
|
||||
import type { Entity, User as LysandUser } from "@lysand-org/federation/types";
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
type InferInsertModel,
|
||||
type SQL,
|
||||
|
|
@ -23,7 +31,6 @@ import {
|
|||
sql,
|
||||
} from "drizzle-orm";
|
||||
import { htmlToText } from "html-to-text";
|
||||
import { objectToInboxRequest } from "~/classes/functions/federation";
|
||||
import {
|
||||
type UserWithRelations,
|
||||
findManyUsers,
|
||||
|
|
@ -341,9 +348,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
uri: string,
|
||||
instance: Instance,
|
||||
): Promise<User> {
|
||||
const { data: json } = await new FederationRequester().get<
|
||||
Partial<LysandUser>
|
||||
>(uri, {
|
||||
const requester = await User.getServerActor().getFederationRequester();
|
||||
const { data: json } = await requester.get<Partial<LysandUser>>(uri, {
|
||||
// @ts-expect-error Bun extension
|
||||
proxy: config.http.proxy.address,
|
||||
});
|
||||
|
|
@ -617,7 +623,66 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
return updated.data;
|
||||
}
|
||||
|
||||
async federateToFollowers(object: Entity) {
|
||||
/**
|
||||
* Signs a Lysand entity with that user's private key
|
||||
*
|
||||
* @param entity Entity to sign
|
||||
* @param signatureUrl URL to embed in signature (must be the same URI of queries made with this signature)
|
||||
* @param signatureMethod HTTP method to embed in signature (default: POST)
|
||||
* @returns The signed string and headers to send with the request
|
||||
*/
|
||||
async sign(
|
||||
entity: Entity,
|
||||
signatureUrl: string | URL,
|
||||
signatureMethod: HttpVerb = "POST",
|
||||
): Promise<{
|
||||
headers: Headers;
|
||||
signedString: string;
|
||||
}> {
|
||||
const signatureConstructor = await SignatureConstructor.fromStringKey(
|
||||
this.data.privateKey ?? "",
|
||||
this.getUri(),
|
||||
);
|
||||
|
||||
const output = await signatureConstructor.sign(
|
||||
signatureMethod,
|
||||
new URL(signatureUrl),
|
||||
JSON.stringify(entity),
|
||||
);
|
||||
|
||||
if (config.debug.federation) {
|
||||
const logger = getLogger("federation");
|
||||
|
||||
// Log public key
|
||||
logger.debug`Sender public key: ${this.data.publicKey}`;
|
||||
|
||||
// Log signed string
|
||||
logger.debug`Signed string:\n${output.signedString}`;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the appropriate Lysand SDK requester with this user's private key
|
||||
*
|
||||
* @returns The requester
|
||||
*/
|
||||
async getFederationRequester(): Promise<FederationRequester> {
|
||||
const signatureConstructor = await SignatureConstructor.fromStringKey(
|
||||
this.data.privateKey ?? "",
|
||||
this.getUri(),
|
||||
);
|
||||
|
||||
return new FederationRequester(signatureConstructor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Federates an entity to all followers of the user
|
||||
*
|
||||
* @param entity Entity to federate
|
||||
*/
|
||||
async federateToFollowers(entity: Entity): Promise<void> {
|
||||
// Get followers
|
||||
const followers = await User.manyFromSql(
|
||||
and(
|
||||
|
|
@ -627,19 +692,45 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
);
|
||||
|
||||
for (const follower of followers) {
|
||||
const federationRequest = await objectToInboxRequest(
|
||||
object,
|
||||
this,
|
||||
follower,
|
||||
);
|
||||
|
||||
// FIXME: Add to new queue system when it's implemented
|
||||
fetch(federationRequest, {
|
||||
proxy: config.http.proxy.address,
|
||||
});
|
||||
await this.federateToUser(entity, follower);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Federates an entity to any user.
|
||||
*
|
||||
* @param entity Entity to federate
|
||||
* @param user User to federate to
|
||||
* @returns Whether the federation was successful
|
||||
*/
|
||||
async federateToUser(entity: Entity, user: User): Promise<{ ok: boolean }> {
|
||||
const { headers } = await this.sign(
|
||||
entity,
|
||||
user.data.endpoints?.inbox ?? "",
|
||||
);
|
||||
|
||||
try {
|
||||
await new FederationRequester().post(
|
||||
user.data.endpoints?.inbox ?? "",
|
||||
entity,
|
||||
{
|
||||
// @ts-expect-error Bun extension
|
||||
proxy: config.http.proxy.address,
|
||||
headers,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
getLogger("federation")
|
||||
.error`Federating ${chalk.gray(entity.type)} to ${user.getUri()} ${chalk.bold.red("failed")}`;
|
||||
getLogger("federation").error`${e}`;
|
||||
sentry?.captureException(e);
|
||||
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
toApi(isOwnAccount = false): ApiAccount {
|
||||
const user = this.data;
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue