mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
chore(federation): 👽 Initial Versia Working Draft 4.0 support
This commit is contained in:
parent
9c71c3fe51
commit
c3fa867e74
22 changed files with 269 additions and 197 deletions
|
|
@ -3,7 +3,7 @@ import type {
|
|||
AsyncAttachment as ApiAsyncAttachment,
|
||||
Attachment as ApiAttachment,
|
||||
} from "@lysand-org/client/types";
|
||||
import type { ContentFormat } from "@lysand-org/federation/types";
|
||||
import type { ContentFormat } from "@versia/federation/types";
|
||||
import {
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
|
|
@ -201,7 +201,9 @@ export class Attachment extends BaseInterface<typeof Attachments> {
|
|||
return {
|
||||
[this.data.mimeType]: {
|
||||
content: this.data.url,
|
||||
blurhash: this.data.blurhash ?? undefined,
|
||||
remote: true,
|
||||
// TODO: Replace BlurHash with thumbhash
|
||||
// thumbhash: this.data.blurhash ?? undefined,
|
||||
description: this.data.description ?? undefined,
|
||||
duration: this.data.duration ?? undefined,
|
||||
fps: this.data.fps ?? undefined,
|
||||
|
|
@ -233,7 +235,7 @@ export class Attachment extends BaseInterface<typeof Attachments> {
|
|||
size: value.size || undefined,
|
||||
width: value.width || undefined,
|
||||
sha256: value.hash?.sha256 || undefined,
|
||||
blurhash: value.blurhash || undefined,
|
||||
// blurhash: value.blurhash || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
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 { CustomEmojiExtension } from "@versia/federation/types";
|
||||
import {
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
|
|
@ -196,6 +196,7 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiWithInstance> {
|
|||
[this.data.contentType]: {
|
||||
content: this.data.url,
|
||||
description: this.data.alt || undefined,
|
||||
remote: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import {
|
|||
EntityValidator,
|
||||
type ResponseError,
|
||||
type ValidationError,
|
||||
} from "@lysand-org/federation";
|
||||
import type { ServerMetadata } from "@lysand-org/federation/types";
|
||||
} from "@versia/federation";
|
||||
import type { InstanceMetadata } from "@versia/federation/types";
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
type InferInsertModel,
|
||||
|
|
@ -132,7 +132,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
|||
}
|
||||
|
||||
static async fetchMetadata(url: string): Promise<{
|
||||
metadata: ServerMetadata;
|
||||
metadata: InstanceMetadata;
|
||||
protocol: "versia" | "activitypub";
|
||||
} | null> {
|
||||
const origin = new URL(url).origin;
|
||||
|
|
@ -167,7 +167,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
|||
}
|
||||
|
||||
try {
|
||||
const metadata = await new EntityValidator().ServerMetadata(
|
||||
const metadata = await new EntityValidator().InstanceMetadata(
|
||||
data,
|
||||
);
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
|||
|
||||
private static async fetchActivityPubMetadata(
|
||||
url: string,
|
||||
): Promise<ServerMetadata | null> {
|
||||
): Promise<InstanceMetadata | null> {
|
||||
const origin = new URL(url).origin;
|
||||
const wellKnownUrl = new URL("/.well-known/nodeinfo", origin);
|
||||
|
||||
|
|
@ -285,14 +285,30 @@ export class Instance extends BaseInterface<typeof Instances> {
|
|||
return {
|
||||
name:
|
||||
metadata.metadata.nodeName || metadata.metadata.title || "",
|
||||
version: metadata.software.version,
|
||||
description:
|
||||
metadata.metadata.nodeDescription ||
|
||||
metadata.metadata.description ||
|
||||
"",
|
||||
logo: undefined,
|
||||
type: "ServerMetadata",
|
||||
supported_extensions: [],
|
||||
description: {
|
||||
"text/plain": {
|
||||
content:
|
||||
metadata.metadata.nodeDescription ||
|
||||
metadata.metadata.description ||
|
||||
"",
|
||||
remote: false,
|
||||
},
|
||||
},
|
||||
type: "InstanceMetadata",
|
||||
software: {
|
||||
name: "Unknown ActivityPub software",
|
||||
version: metadata.software.version,
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
public_key: {
|
||||
key: "",
|
||||
algorithm: "ed25519",
|
||||
},
|
||||
host: new URL(url).host,
|
||||
compatibility: {
|
||||
extensions: [],
|
||||
versions: [],
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error`Failed to fetch ActivityPub metadata for instance ${chalk.bold(
|
||||
|
|
@ -326,7 +342,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
|||
return Instance.insert({
|
||||
baseUrl: host,
|
||||
name: metadata.name,
|
||||
version: metadata.version,
|
||||
version: metadata.software.version,
|
||||
logo: metadata.logo,
|
||||
protocol: protocol,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ import type {
|
|||
Attachment as ApiAttachment,
|
||||
Status as ApiStatus,
|
||||
} from "@lysand-org/client/types";
|
||||
import { EntityValidator } from "@lysand-org/federation";
|
||||
import { EntityValidator } from "@versia/federation";
|
||||
import type {
|
||||
ContentFormat,
|
||||
Delete as VersiaDelete,
|
||||
Note as VersiaNote,
|
||||
} from "@lysand-org/federation/types";
|
||||
} from "@versia/federation/types";
|
||||
import {
|
||||
type InferInsertModel,
|
||||
type SQL,
|
||||
|
|
@ -666,14 +667,26 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
|||
}
|
||||
}
|
||||
|
||||
let visibility = note.group
|
||||
? ["public", "followers"].includes(note.group)
|
||||
? (note.group as "public" | "private")
|
||||
: ("url" as const)
|
||||
: ("direct" as const);
|
||||
|
||||
if (visibility === "url") {
|
||||
// TODO: Implement groups
|
||||
visibility = "direct";
|
||||
}
|
||||
|
||||
const newData = {
|
||||
author,
|
||||
content: note.content ?? {
|
||||
"text/plain": {
|
||||
content: "",
|
||||
remote: false,
|
||||
},
|
||||
},
|
||||
visibility: note.visibility as ApiStatus["visibility"],
|
||||
visibility: visibility as ApiStatus["visibility"],
|
||||
isSensitive: note.is_sensitive ?? false,
|
||||
spoilerText: note.subject ?? "",
|
||||
emojis,
|
||||
|
|
@ -885,6 +898,19 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
|||
).toString();
|
||||
}
|
||||
|
||||
deleteToVersia(): VersiaDelete {
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
return {
|
||||
type: "Delete",
|
||||
id,
|
||||
author: this.author.getUri(),
|
||||
deleted_type: "Note",
|
||||
target: this.getUri(),
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a note to the Versia format
|
||||
* @returns The note in the Versia format
|
||||
|
|
@ -900,9 +926,11 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
|||
content: {
|
||||
"text/html": {
|
||||
content: status.content,
|
||||
remote: false,
|
||||
},
|
||||
"text/plain": {
|
||||
content: htmlToText(status.content),
|
||||
remote: false,
|
||||
},
|
||||
},
|
||||
attachments: (status.attachments ?? []).map((attachment) =>
|
||||
|
|
@ -917,11 +945,8 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
|||
replies_to:
|
||||
Note.getUri(status.replyId, status.reply?.uri) ?? undefined,
|
||||
subject: status.spoilerText,
|
||||
visibility: status.visibility as
|
||||
| "public"
|
||||
| "unlisted"
|
||||
| "private"
|
||||
| "direct",
|
||||
// TODO: Refactor as part of groups
|
||||
group: status.visibility === "public" ? "public" : "followers",
|
||||
extensions: {
|
||||
"org.lysand:custom_emojis": {
|
||||
emojis: status.emojis.map((emoji) =>
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import {
|
|||
FederationRequester,
|
||||
type HttpVerb,
|
||||
SignatureConstructor,
|
||||
} from "@lysand-org/federation";
|
||||
import type { Entity, User as VersiaUser } from "@lysand-org/federation/types";
|
||||
} from "@versia/federation";
|
||||
import type { User as VersiaUser } from "@versia/federation/types";
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
type InferInsertModel,
|
||||
|
|
@ -48,7 +48,8 @@ import {
|
|||
Users,
|
||||
} from "~/drizzle/schema";
|
||||
import { type Config, config } from "~/packages/config-manager";
|
||||
import { undoFederationRequest } from "../../classes/functions/federation.ts";
|
||||
import type { KnownEntity } from "~/types/api.ts";
|
||||
import { unfollowFederationRequest } from "../../classes/functions/federation.ts";
|
||||
import { BaseInterface } from "./base";
|
||||
import { Emoji } from "./emoji";
|
||||
import { Instance } from "./instance";
|
||||
|
|
@ -259,13 +260,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
if (followee.isRemote()) {
|
||||
// TODO: This should reschedule for a later time and maybe notify the server admin if it fails too often
|
||||
const { ok } = await this.federateToUser(
|
||||
undoFederationRequest(
|
||||
this,
|
||||
new URL(
|
||||
`/follows/${relationship.id}`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
),
|
||||
unfollowFederationRequest(this, followee),
|
||||
followee,
|
||||
);
|
||||
|
||||
|
|
@ -450,7 +445,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
const user = await User.fromVersia(data, instance);
|
||||
|
||||
const userEmojis =
|
||||
data.extensions?.["org.lysand:custom_emojis"]?.emojis ?? [];
|
||||
data.extensions?.["pub.versia:custom_emojis"]?.emojis ?? [];
|
||||
const emojis = await Promise.all(
|
||||
userEmojis.map((emoji) => Emoji.fromVersia(emoji, instance.id)),
|
||||
);
|
||||
|
|
@ -484,13 +479,14 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
uri: user.uri,
|
||||
createdAt: new Date(user.created_at).toISOString(),
|
||||
endpoints: {
|
||||
dislikes: user.dislikes,
|
||||
featured: user.featured,
|
||||
likes: user.likes,
|
||||
followers: user.followers,
|
||||
following: user.following,
|
||||
dislikes:
|
||||
user.collections["pub.versia:likes/Dislikes"] ?? undefined,
|
||||
featured: user.collections.featured,
|
||||
likes: user.collections["pub.versia:likes/Likes"] ?? undefined,
|
||||
followers: user.collections.followers,
|
||||
following: user.collections.following,
|
||||
inbox: user.inbox,
|
||||
outbox: user.outbox,
|
||||
outbox: user.collections.outbox,
|
||||
},
|
||||
fields: user.fields ?? [],
|
||||
updatedAt: new Date(user.created_at).toISOString(),
|
||||
|
|
@ -503,7 +499,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
: "",
|
||||
displayName: user.display_name ?? "",
|
||||
note: getBestContentType(user.bio).content,
|
||||
publicKey: user.public_key.public_key,
|
||||
publicKey: user.public_key.key,
|
||||
source: {
|
||||
language: null,
|
||||
note: "",
|
||||
|
|
@ -722,7 +718,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
* @returns The signed string and headers to send with the request
|
||||
*/
|
||||
async sign(
|
||||
entity: Entity,
|
||||
entity: KnownEntity,
|
||||
signatureUrl: string | URL,
|
||||
signatureMethod: HttpVerb = "POST",
|
||||
): Promise<{
|
||||
|
|
@ -772,7 +768,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
*
|
||||
* @param entity Entity to federate
|
||||
*/
|
||||
async federateToFollowers(entity: Entity): Promise<void> {
|
||||
async federateToFollowers(entity: KnownEntity): Promise<void> {
|
||||
// Get followers
|
||||
const followers = await User.manyFromSql(
|
||||
and(
|
||||
|
|
@ -793,7 +789,10 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
* @param user User to federate to
|
||||
* @returns Whether the federation was successful
|
||||
*/
|
||||
async federateToUser(entity: Entity, user: User): Promise<{ ok: boolean }> {
|
||||
async federateToUser(
|
||||
entity: KnownEntity,
|
||||
user: User,
|
||||
): Promise<{ ok: boolean }> {
|
||||
const { headers } = await this.sign(
|
||||
entity,
|
||||
user.data.endpoints?.inbox ?? "",
|
||||
|
|
@ -908,40 +907,44 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
bio: {
|
||||
"text/html": {
|
||||
content: user.note,
|
||||
remote: false,
|
||||
},
|
||||
"text/plain": {
|
||||
content: htmlToText(user.note),
|
||||
remote: false,
|
||||
},
|
||||
},
|
||||
created_at: new Date(user.createdAt).toISOString(),
|
||||
dislikes: new URL(
|
||||
`/users/${user.id}/dislikes`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
featured: new URL(
|
||||
`/users/${user.id}/featured`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
likes: new URL(
|
||||
`/users/${user.id}/likes`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
followers: new URL(
|
||||
`/users/${user.id}/followers`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
following: new URL(
|
||||
`/users/${user.id}/following`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
collections: {
|
||||
featured: new URL(
|
||||
`/users/${user.id}/featured`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
"pub.versia:likes/Likes": new URL(
|
||||
`/users/${user.id}/likes`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
"pub.versia:likes/Dislikes": new URL(
|
||||
`/users/${user.id}/dislikes`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
followers: new URL(
|
||||
`/users/${user.id}/followers`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
following: new URL(
|
||||
`/users/${user.id}/following`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
outbox: new URL(
|
||||
`/users/${user.id}/outbox`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
},
|
||||
inbox: new URL(
|
||||
`/users/${user.id}/inbox`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
outbox: new URL(
|
||||
`/users/${user.id}/outbox`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
indexable: false,
|
||||
username: user.username,
|
||||
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
|
||||
|
|
@ -953,7 +956,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
`/users/${user.id}`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
public_key: user.publicKey,
|
||||
key: user.publicKey,
|
||||
algorithm: "ed25519",
|
||||
},
|
||||
extensions: {
|
||||
"org.lysand:custom_emojis": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue