refactor(federation): 🔥 Remove old code and simplify federation requests

This commit is contained in:
Jesse Wierzbinski 2024-07-26 18:51:39 +02:00
parent ad9ed2598c
commit 2f823317c2
No known key found for this signature in database
18 changed files with 182 additions and 302 deletions

View file

@ -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

View file

@ -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;

View file

@ -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,
});

View file

@ -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 {