mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
chore(federation): 👽 Initial Versia Working Draft 4.0 support
This commit is contained in:
parent
9c71c3fe51
commit
c3fa867e74
|
|
@ -1,15 +1,16 @@
|
||||||
import type { Undo } from "@lysand-org/federation/types";
|
import type { Unfollow } from "@versia/federation/types";
|
||||||
import { config } from "~/packages/config-manager/index";
|
|
||||||
import type { User } from "~/packages/database-interface/user";
|
import type { User } from "~/packages/database-interface/user";
|
||||||
|
|
||||||
export const undoFederationRequest = (undoer: User, uri: string): Undo => {
|
export const unfollowFederationRequest = (
|
||||||
|
unfollower: User,
|
||||||
|
unfollowing: User,
|
||||||
|
): Unfollow => {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
return {
|
return {
|
||||||
type: "Undo",
|
type: "Unfollow",
|
||||||
id,
|
id,
|
||||||
author: undoer.getUri(),
|
author: unfollower.getUri(),
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
object: uri,
|
followee: unfollowing.getUri(),
|
||||||
uri: new URL(`/undos/${id}`, config.http.base_url).toString(),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Like } from "@lysand-org/federation/types";
|
import type { LikeExtension } from "@versia/federation/types";
|
||||||
import { type InferSelectModel, and, eq } from "drizzle-orm";
|
import { type InferSelectModel, and, eq } from "drizzle-orm";
|
||||||
import { db } from "~/drizzle/db";
|
import { db } from "~/drizzle/db";
|
||||||
import { Likes, Notifications } from "~/drizzle/schema";
|
import { Likes, Notifications } from "~/drizzle/schema";
|
||||||
|
|
@ -11,15 +11,15 @@ export type LikeType = InferSelectModel<typeof Likes>;
|
||||||
/**
|
/**
|
||||||
* Represents a Like entity in the database.
|
* Represents a Like entity in the database.
|
||||||
*/
|
*/
|
||||||
export const likeToVersia = (like: LikeType): Like => {
|
export const likeToVersia = (like: LikeType): LikeExtension => {
|
||||||
return {
|
return {
|
||||||
id: like.id,
|
id: like.id,
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
||||||
author: (like as any).liker?.uri,
|
author: (like as any).liker?.uri,
|
||||||
type: "Like",
|
type: "pub.versia:likes/Like",
|
||||||
created_at: new Date(like.createdAt).toISOString(),
|
created_at: new Date(like.createdAt).toISOString(),
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
||||||
object: (like as any).liked?.uri,
|
liked: (like as any).liked?.uri,
|
||||||
uri: new URL(`/objects/${like.id}`, config.http.base_url).toString(),
|
uri: new URL(`/objects/${like.id}`, config.http.base_url).toString(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { mentionValidator } from "@/api";
|
import { mentionValidator } from "@/api";
|
||||||
import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization";
|
import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization";
|
||||||
import markdownItTaskLists from "@hackmd/markdown-it-task-lists";
|
import markdownItTaskLists from "@hackmd/markdown-it-task-lists";
|
||||||
import type { ContentFormat } from "@lysand-org/federation/types";
|
import type { ContentFormat } from "@versia/federation/types";
|
||||||
import {
|
import {
|
||||||
type InferSelectModel,
|
type InferSelectModel,
|
||||||
and,
|
and,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import type {
|
||||||
Follow,
|
Follow,
|
||||||
FollowAccept,
|
FollowAccept,
|
||||||
FollowReject,
|
FollowReject,
|
||||||
} from "@lysand-org/federation/types";
|
} from "@versia/federation/types";
|
||||||
import { type InferSelectModel, eq, sql } from "drizzle-orm";
|
import { type InferSelectModel, eq, sql } from "drizzle-orm";
|
||||||
import { db } from "~/drizzle/db";
|
import { db } from "~/drizzle/db";
|
||||||
import {
|
import {
|
||||||
|
|
@ -12,7 +12,6 @@ import {
|
||||||
Tokens,
|
Tokens,
|
||||||
type Users,
|
type Users,
|
||||||
} from "~/drizzle/schema";
|
} from "~/drizzle/schema";
|
||||||
import { config } from "~/packages/config-manager/index";
|
|
||||||
import type { EmojiWithInstance } from "~/packages/database-interface/emoji";
|
import type { EmojiWithInstance } from "~/packages/database-interface/emoji";
|
||||||
import { User } from "~/packages/database-interface/user";
|
import { User } from "~/packages/database-interface/user";
|
||||||
import type { Application } from "./application";
|
import type { Application } from "./application";
|
||||||
|
|
@ -282,7 +281,6 @@ export const followRequestToVersia = (
|
||||||
author: follower.getUri(),
|
author: follower.getUri(),
|
||||||
followee: followee.getUri(),
|
followee: followee.getUri(),
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
uri: new URL(`/follows/${id}`, config.http.base_url).toString(),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -310,7 +308,6 @@ export const followAcceptToVersia = (
|
||||||
author: followee.getUri(),
|
author: followee.getUri(),
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
follower: follower.getUri(),
|
follower: follower.getUri(),
|
||||||
uri: new URL(`/follows/${id}`, config.http.base_url).toString(),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Source as ApiSource } from "@lysand-org/client/types";
|
import type { Source as ApiSource } from "@lysand-org/client/types";
|
||||||
import type { ContentFormat } from "@lysand-org/federation/types";
|
import type { ContentFormat } from "@versia/federation/types";
|
||||||
import type { Challenge } from "altcha-lib/types";
|
import type { Challenge } from "altcha-lib/types";
|
||||||
import { relations, sql } from "drizzle-orm";
|
import { relations, sql } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
|
|
@ -381,9 +381,9 @@ export const Users = pgTable(
|
||||||
}[]
|
}[]
|
||||||
>(),
|
>(),
|
||||||
endpoints: jsonb("endpoints").$type<Partial<{
|
endpoints: jsonb("endpoints").$type<Partial<{
|
||||||
dislikes: string;
|
dislikes?: string;
|
||||||
featured: string;
|
featured: string;
|
||||||
likes: string;
|
likes?: string;
|
||||||
followers: string;
|
followers: string;
|
||||||
following: string;
|
following: string;
|
||||||
inbox: string;
|
inbox: string;
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@
|
||||||
"@oclif/core": "^4.0.19",
|
"@oclif/core": "^4.0.19",
|
||||||
"@sentry/bun": "^8.26.0",
|
"@sentry/bun": "^8.26.0",
|
||||||
"@tufjs/canonical-json": "^2.0.0",
|
"@tufjs/canonical-json": "^2.0.0",
|
||||||
|
"@versia/federation": "^0.1.0-rc.0",
|
||||||
"altcha-lib": "^0.5.1",
|
"altcha-lib": "^0.5.1",
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"bullmq": "^5.12.10",
|
"bullmq": "^5.12.10",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import type {
|
||||||
AsyncAttachment as ApiAsyncAttachment,
|
AsyncAttachment as ApiAsyncAttachment,
|
||||||
Attachment as ApiAttachment,
|
Attachment as ApiAttachment,
|
||||||
} from "@lysand-org/client/types";
|
} from "@lysand-org/client/types";
|
||||||
import type { ContentFormat } from "@lysand-org/federation/types";
|
import type { ContentFormat } from "@versia/federation/types";
|
||||||
import {
|
import {
|
||||||
type InferInsertModel,
|
type InferInsertModel,
|
||||||
type InferSelectModel,
|
type InferSelectModel,
|
||||||
|
|
@ -201,7 +201,9 @@ export class Attachment extends BaseInterface<typeof Attachments> {
|
||||||
return {
|
return {
|
||||||
[this.data.mimeType]: {
|
[this.data.mimeType]: {
|
||||||
content: this.data.url,
|
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,
|
description: this.data.description ?? undefined,
|
||||||
duration: this.data.duration ?? undefined,
|
duration: this.data.duration ?? undefined,
|
||||||
fps: this.data.fps ?? undefined,
|
fps: this.data.fps ?? undefined,
|
||||||
|
|
@ -233,7 +235,7 @@ export class Attachment extends BaseInterface<typeof Attachments> {
|
||||||
size: value.size || undefined,
|
size: value.size || undefined,
|
||||||
width: value.width || undefined,
|
width: value.width || undefined,
|
||||||
sha256: value.hash?.sha256 || undefined,
|
sha256: value.hash?.sha256 || undefined,
|
||||||
blurhash: value.blurhash || undefined,
|
// blurhash: value.blurhash || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { emojiValidatorWithColons } from "@/api";
|
import { emojiValidatorWithColons } from "@/api";
|
||||||
import { proxyUrl } from "@/response";
|
import { proxyUrl } from "@/response";
|
||||||
import type { Emoji as ApiEmoji } from "@lysand-org/client/types";
|
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 {
|
import {
|
||||||
type InferInsertModel,
|
type InferInsertModel,
|
||||||
type InferSelectModel,
|
type InferSelectModel,
|
||||||
|
|
@ -196,6 +196,7 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiWithInstance> {
|
||||||
[this.data.contentType]: {
|
[this.data.contentType]: {
|
||||||
content: this.data.url,
|
content: this.data.url,
|
||||||
description: this.data.alt || undefined,
|
description: this.data.alt || undefined,
|
||||||
|
remote: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import {
|
||||||
EntityValidator,
|
EntityValidator,
|
||||||
type ResponseError,
|
type ResponseError,
|
||||||
type ValidationError,
|
type ValidationError,
|
||||||
} from "@lysand-org/federation";
|
} from "@versia/federation";
|
||||||
import type { ServerMetadata } from "@lysand-org/federation/types";
|
import type { InstanceMetadata } from "@versia/federation/types";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import {
|
import {
|
||||||
type InferInsertModel,
|
type InferInsertModel,
|
||||||
|
|
@ -132,7 +132,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetchMetadata(url: string): Promise<{
|
static async fetchMetadata(url: string): Promise<{
|
||||||
metadata: ServerMetadata;
|
metadata: InstanceMetadata;
|
||||||
protocol: "versia" | "activitypub";
|
protocol: "versia" | "activitypub";
|
||||||
} | null> {
|
} | null> {
|
||||||
const origin = new URL(url).origin;
|
const origin = new URL(url).origin;
|
||||||
|
|
@ -167,7 +167,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const metadata = await new EntityValidator().ServerMetadata(
|
const metadata = await new EntityValidator().InstanceMetadata(
|
||||||
data,
|
data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -188,7 +188,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
|
|
||||||
private static async fetchActivityPubMetadata(
|
private static async fetchActivityPubMetadata(
|
||||||
url: string,
|
url: string,
|
||||||
): Promise<ServerMetadata | null> {
|
): Promise<InstanceMetadata | null> {
|
||||||
const origin = new URL(url).origin;
|
const origin = new URL(url).origin;
|
||||||
const wellKnownUrl = new URL("/.well-known/nodeinfo", origin);
|
const wellKnownUrl = new URL("/.well-known/nodeinfo", origin);
|
||||||
|
|
||||||
|
|
@ -285,14 +285,30 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
return {
|
return {
|
||||||
name:
|
name:
|
||||||
metadata.metadata.nodeName || metadata.metadata.title || "",
|
metadata.metadata.nodeName || metadata.metadata.title || "",
|
||||||
version: metadata.software.version,
|
description: {
|
||||||
description:
|
"text/plain": {
|
||||||
|
content:
|
||||||
metadata.metadata.nodeDescription ||
|
metadata.metadata.nodeDescription ||
|
||||||
metadata.metadata.description ||
|
metadata.metadata.description ||
|
||||||
"",
|
"",
|
||||||
logo: undefined,
|
remote: false,
|
||||||
type: "ServerMetadata",
|
},
|
||||||
supported_extensions: [],
|
},
|
||||||
|
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) {
|
} catch (error) {
|
||||||
logger.error`Failed to fetch ActivityPub metadata for instance ${chalk.bold(
|
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({
|
return Instance.insert({
|
||||||
baseUrl: host,
|
baseUrl: host,
|
||||||
name: metadata.name,
|
name: metadata.name,
|
||||||
version: metadata.version,
|
version: metadata.software.version,
|
||||||
logo: metadata.logo,
|
logo: metadata.logo,
|
||||||
protocol: protocol,
|
protocol: protocol,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,12 @@ import type {
|
||||||
Attachment as ApiAttachment,
|
Attachment as ApiAttachment,
|
||||||
Status as ApiStatus,
|
Status as ApiStatus,
|
||||||
} from "@lysand-org/client/types";
|
} from "@lysand-org/client/types";
|
||||||
import { EntityValidator } from "@lysand-org/federation";
|
import { EntityValidator } from "@versia/federation";
|
||||||
import type {
|
import type {
|
||||||
ContentFormat,
|
ContentFormat,
|
||||||
|
Delete as VersiaDelete,
|
||||||
Note as VersiaNote,
|
Note as VersiaNote,
|
||||||
} from "@lysand-org/federation/types";
|
} from "@versia/federation/types";
|
||||||
import {
|
import {
|
||||||
type InferInsertModel,
|
type InferInsertModel,
|
||||||
type SQL,
|
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 = {
|
const newData = {
|
||||||
author,
|
author,
|
||||||
content: note.content ?? {
|
content: note.content ?? {
|
||||||
"text/plain": {
|
"text/plain": {
|
||||||
content: "",
|
content: "",
|
||||||
|
remote: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visibility: note.visibility as ApiStatus["visibility"],
|
visibility: visibility as ApiStatus["visibility"],
|
||||||
isSensitive: note.is_sensitive ?? false,
|
isSensitive: note.is_sensitive ?? false,
|
||||||
spoilerText: note.subject ?? "",
|
spoilerText: note.subject ?? "",
|
||||||
emojis,
|
emojis,
|
||||||
|
|
@ -885,6 +898,19 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
).toString();
|
).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
|
* Convert a note to the Versia format
|
||||||
* @returns The note in the Versia format
|
* @returns The note in the Versia format
|
||||||
|
|
@ -900,9 +926,11 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
content: {
|
content: {
|
||||||
"text/html": {
|
"text/html": {
|
||||||
content: status.content,
|
content: status.content,
|
||||||
|
remote: false,
|
||||||
},
|
},
|
||||||
"text/plain": {
|
"text/plain": {
|
||||||
content: htmlToText(status.content),
|
content: htmlToText(status.content),
|
||||||
|
remote: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
attachments: (status.attachments ?? []).map((attachment) =>
|
attachments: (status.attachments ?? []).map((attachment) =>
|
||||||
|
|
@ -917,11 +945,8 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
replies_to:
|
replies_to:
|
||||||
Note.getUri(status.replyId, status.reply?.uri) ?? undefined,
|
Note.getUri(status.replyId, status.reply?.uri) ?? undefined,
|
||||||
subject: status.spoilerText,
|
subject: status.spoilerText,
|
||||||
visibility: status.visibility as
|
// TODO: Refactor as part of groups
|
||||||
| "public"
|
group: status.visibility === "public" ? "public" : "followers",
|
||||||
| "unlisted"
|
|
||||||
| "private"
|
|
||||||
| "direct",
|
|
||||||
extensions: {
|
extensions: {
|
||||||
"org.lysand:custom_emojis": {
|
"org.lysand:custom_emojis": {
|
||||||
emojis: status.emojis.map((emoji) =>
|
emojis: status.emojis.map((emoji) =>
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import {
|
||||||
FederationRequester,
|
FederationRequester,
|
||||||
type HttpVerb,
|
type HttpVerb,
|
||||||
SignatureConstructor,
|
SignatureConstructor,
|
||||||
} from "@lysand-org/federation";
|
} from "@versia/federation";
|
||||||
import type { Entity, User as VersiaUser } from "@lysand-org/federation/types";
|
import type { User as VersiaUser } from "@versia/federation/types";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import {
|
import {
|
||||||
type InferInsertModel,
|
type InferInsertModel,
|
||||||
|
|
@ -48,7 +48,8 @@ import {
|
||||||
Users,
|
Users,
|
||||||
} from "~/drizzle/schema";
|
} from "~/drizzle/schema";
|
||||||
import { type Config, config } from "~/packages/config-manager";
|
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 { BaseInterface } from "./base";
|
||||||
import { Emoji } from "./emoji";
|
import { Emoji } from "./emoji";
|
||||||
import { Instance } from "./instance";
|
import { Instance } from "./instance";
|
||||||
|
|
@ -259,13 +260,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
if (followee.isRemote()) {
|
if (followee.isRemote()) {
|
||||||
// TODO: This should reschedule for a later time and maybe notify the server admin if it fails too often
|
// TODO: This should reschedule for a later time and maybe notify the server admin if it fails too often
|
||||||
const { ok } = await this.federateToUser(
|
const { ok } = await this.federateToUser(
|
||||||
undoFederationRequest(
|
unfollowFederationRequest(this, followee),
|
||||||
this,
|
|
||||||
new URL(
|
|
||||||
`/follows/${relationship.id}`,
|
|
||||||
config.http.base_url,
|
|
||||||
).toString(),
|
|
||||||
),
|
|
||||||
followee,
|
followee,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -450,7 +445,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
const user = await User.fromVersia(data, instance);
|
const user = await User.fromVersia(data, instance);
|
||||||
|
|
||||||
const userEmojis =
|
const userEmojis =
|
||||||
data.extensions?.["org.lysand:custom_emojis"]?.emojis ?? [];
|
data.extensions?.["pub.versia:custom_emojis"]?.emojis ?? [];
|
||||||
const emojis = await Promise.all(
|
const emojis = await Promise.all(
|
||||||
userEmojis.map((emoji) => Emoji.fromVersia(emoji, instance.id)),
|
userEmojis.map((emoji) => Emoji.fromVersia(emoji, instance.id)),
|
||||||
);
|
);
|
||||||
|
|
@ -484,13 +479,14 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
uri: user.uri,
|
uri: user.uri,
|
||||||
createdAt: new Date(user.created_at).toISOString(),
|
createdAt: new Date(user.created_at).toISOString(),
|
||||||
endpoints: {
|
endpoints: {
|
||||||
dislikes: user.dislikes,
|
dislikes:
|
||||||
featured: user.featured,
|
user.collections["pub.versia:likes/Dislikes"] ?? undefined,
|
||||||
likes: user.likes,
|
featured: user.collections.featured,
|
||||||
followers: user.followers,
|
likes: user.collections["pub.versia:likes/Likes"] ?? undefined,
|
||||||
following: user.following,
|
followers: user.collections.followers,
|
||||||
|
following: user.collections.following,
|
||||||
inbox: user.inbox,
|
inbox: user.inbox,
|
||||||
outbox: user.outbox,
|
outbox: user.collections.outbox,
|
||||||
},
|
},
|
||||||
fields: user.fields ?? [],
|
fields: user.fields ?? [],
|
||||||
updatedAt: new Date(user.created_at).toISOString(),
|
updatedAt: new Date(user.created_at).toISOString(),
|
||||||
|
|
@ -503,7 +499,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
: "",
|
: "",
|
||||||
displayName: user.display_name ?? "",
|
displayName: user.display_name ?? "",
|
||||||
note: getBestContentType(user.bio).content,
|
note: getBestContentType(user.bio).content,
|
||||||
publicKey: user.public_key.public_key,
|
publicKey: user.public_key.key,
|
||||||
source: {
|
source: {
|
||||||
language: null,
|
language: null,
|
||||||
note: "",
|
note: "",
|
||||||
|
|
@ -722,7 +718,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
* @returns The signed string and headers to send with the request
|
* @returns The signed string and headers to send with the request
|
||||||
*/
|
*/
|
||||||
async sign(
|
async sign(
|
||||||
entity: Entity,
|
entity: KnownEntity,
|
||||||
signatureUrl: string | URL,
|
signatureUrl: string | URL,
|
||||||
signatureMethod: HttpVerb = "POST",
|
signatureMethod: HttpVerb = "POST",
|
||||||
): Promise<{
|
): Promise<{
|
||||||
|
|
@ -772,7 +768,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
*
|
*
|
||||||
* @param entity Entity to federate
|
* @param entity Entity to federate
|
||||||
*/
|
*/
|
||||||
async federateToFollowers(entity: Entity): Promise<void> {
|
async federateToFollowers(entity: KnownEntity): Promise<void> {
|
||||||
// Get followers
|
// Get followers
|
||||||
const followers = await User.manyFromSql(
|
const followers = await User.manyFromSql(
|
||||||
and(
|
and(
|
||||||
|
|
@ -793,7 +789,10 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
* @param user User to federate to
|
* @param user User to federate to
|
||||||
* @returns Whether the federation was successful
|
* @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(
|
const { headers } = await this.sign(
|
||||||
entity,
|
entity,
|
||||||
user.data.endpoints?.inbox ?? "",
|
user.data.endpoints?.inbox ?? "",
|
||||||
|
|
@ -908,24 +907,27 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
bio: {
|
bio: {
|
||||||
"text/html": {
|
"text/html": {
|
||||||
content: user.note,
|
content: user.note,
|
||||||
|
remote: false,
|
||||||
},
|
},
|
||||||
"text/plain": {
|
"text/plain": {
|
||||||
content: htmlToText(user.note),
|
content: htmlToText(user.note),
|
||||||
|
remote: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created_at: new Date(user.createdAt).toISOString(),
|
created_at: new Date(user.createdAt).toISOString(),
|
||||||
dislikes: new URL(
|
collections: {
|
||||||
`/users/${user.id}/dislikes`,
|
|
||||||
config.http.base_url,
|
|
||||||
).toString(),
|
|
||||||
featured: new URL(
|
featured: new URL(
|
||||||
`/users/${user.id}/featured`,
|
`/users/${user.id}/featured`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
).toString(),
|
).toString(),
|
||||||
likes: new URL(
|
"pub.versia:likes/Likes": new URL(
|
||||||
`/users/${user.id}/likes`,
|
`/users/${user.id}/likes`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
).toString(),
|
).toString(),
|
||||||
|
"pub.versia:likes/Dislikes": new URL(
|
||||||
|
`/users/${user.id}/dislikes`,
|
||||||
|
config.http.base_url,
|
||||||
|
).toString(),
|
||||||
followers: new URL(
|
followers: new URL(
|
||||||
`/users/${user.id}/followers`,
|
`/users/${user.id}/followers`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
|
|
@ -934,14 +936,15 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
`/users/${user.id}/following`,
|
`/users/${user.id}/following`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
).toString(),
|
).toString(),
|
||||||
inbox: new URL(
|
|
||||||
`/users/${user.id}/inbox`,
|
|
||||||
config.http.base_url,
|
|
||||||
).toString(),
|
|
||||||
outbox: new URL(
|
outbox: new URL(
|
||||||
`/users/${user.id}/outbox`,
|
`/users/${user.id}/outbox`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
).toString(),
|
).toString(),
|
||||||
|
},
|
||||||
|
inbox: new URL(
|
||||||
|
`/users/${user.id}/inbox`,
|
||||||
|
config.http.base_url,
|
||||||
|
).toString(),
|
||||||
indexable: false,
|
indexable: false,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
|
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
|
||||||
|
|
@ -953,7 +956,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
`/users/${user.id}`,
|
`/users/${user.id}`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
).toString(),
|
).toString(),
|
||||||
public_key: user.publicKey,
|
key: user.publicKey,
|
||||||
|
algorithm: "ed25519",
|
||||||
},
|
},
|
||||||
extensions: {
|
extensions: {
|
||||||
"org.lysand:custom_emojis": {
|
"org.lysand:custom_emojis": {
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,7 @@ export default apiRoute((app) =>
|
||||||
self.note = await contentToHtml({
|
self.note = await contentToHtml({
|
||||||
"text/markdown": {
|
"text/markdown": {
|
||||||
content: note,
|
content: note,
|
||||||
|
remote: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -235,6 +236,7 @@ export default apiRoute((app) =>
|
||||||
{
|
{
|
||||||
"text/markdown": {
|
"text/markdown": {
|
||||||
content: field.name,
|
content: field.name,
|
||||||
|
remote: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
|
|
@ -245,6 +247,7 @@ export default apiRoute((app) =>
|
||||||
{
|
{
|
||||||
"text/markdown": {
|
"text/markdown": {
|
||||||
content: field.value,
|
content: field.value,
|
||||||
|
remote: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
|
|
@ -262,11 +265,13 @@ export default apiRoute((app) =>
|
||||||
key: {
|
key: {
|
||||||
"text/html": {
|
"text/html": {
|
||||||
content: parsedName,
|
content: parsedName,
|
||||||
|
remote: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
"text/html": {
|
"text/html": {
|
||||||
content: parsedValue,
|
content: parsedValue,
|
||||||
|
remote: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import {
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import ISO6391 from "iso-639-1";
|
import ISO6391 from "iso-639-1";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { undoFederationRequest } from "~/classes/functions/federation";
|
|
||||||
import { RolePermissions } from "~/drizzle/schema";
|
import { RolePermissions } from "~/drizzle/schema";
|
||||||
import { config } from "~/packages/config-manager/index";
|
import { config } from "~/packages/config-manager/index";
|
||||||
import { Attachment } from "~/packages/database-interface/attachment";
|
import { Attachment } from "~/packages/database-interface/attachment";
|
||||||
|
|
@ -136,9 +135,7 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
await note.delete();
|
await note.delete();
|
||||||
|
|
||||||
await user.federateToFollowers(
|
await user.federateToFollowers(note.deleteToVersia());
|
||||||
undoFederationRequest(user, note.getUri()),
|
|
||||||
);
|
|
||||||
|
|
||||||
return context.json(await note.toApi(user), 200);
|
return context.json(await note.toApi(user), 200);
|
||||||
}
|
}
|
||||||
|
|
@ -169,6 +166,7 @@ export default apiRoute((app) =>
|
||||||
? {
|
? {
|
||||||
[content_type]: {
|
[content_type]: {
|
||||||
content: statusText,
|
content: statusText,
|
||||||
|
remote: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { apiRoute, applyConfig, auth, handleZodError } from "@/api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { undoFederationRequest } from "~/classes/functions/federation";
|
|
||||||
import { Notes, RolePermissions } from "~/drizzle/schema";
|
import { Notes, RolePermissions } from "~/drizzle/schema";
|
||||||
import { Note } from "~/packages/database-interface/note";
|
import { Note } from "~/packages/database-interface/note";
|
||||||
|
|
||||||
|
|
@ -63,9 +62,7 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
await existingReblog.delete();
|
await existingReblog.delete();
|
||||||
|
|
||||||
await user.federateToFollowers(
|
await user.federateToFollowers(existingReblog.deleteToVersia());
|
||||||
undoFederationRequest(user, existingReblog.getUri()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const newNote = await Note.fromId(id, user.id);
|
const newNote = await Note.fromId(id, user.id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,7 @@ export default apiRoute((app) =>
|
||||||
content: {
|
content: {
|
||||||
[content_type]: {
|
[content_type]: {
|
||||||
content: status ?? "",
|
content: status ?? "",
|
||||||
|
remote: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visibility,
|
visibility,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { apiRoute, applyConfig, handleZodError } from "@/api";
|
import { apiRoute, applyConfig, handleZodError } from "@/api";
|
||||||
import { response } from "@/response";
|
import { response } from "@/response";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import type { Entity } from "@lysand-org/federation/types";
|
|
||||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { type LikeType, likeToVersia } from "~/classes/functions/like";
|
import { type LikeType, likeToVersia } from "~/classes/functions/like";
|
||||||
|
|
@ -10,6 +9,7 @@ import { Notes } from "~/drizzle/schema";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/packages/config-manager";
|
||||||
import { Note } from "~/packages/database-interface/note";
|
import { Note } from "~/packages/database-interface/note";
|
||||||
import { User } from "~/packages/database-interface/user";
|
import { User } from "~/packages/database-interface/user";
|
||||||
|
import type { KnownEntity } from "~/types/api";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -39,7 +39,7 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
let foundObject: Note | LikeType | null = null;
|
let foundObject: Note | LikeType | null = null;
|
||||||
let foundAuthor: User | null = null;
|
let foundAuthor: User | null = null;
|
||||||
let apiObject: Entity | null = null;
|
let apiObject: KnownEntity | null = null;
|
||||||
|
|
||||||
foundObject = await Note.fromSql(
|
foundObject = await Note.fromSql(
|
||||||
and(
|
and(
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import {
|
||||||
EntityValidator,
|
EntityValidator,
|
||||||
RequestParserHandler,
|
RequestParserHandler,
|
||||||
SignatureValidator,
|
SignatureValidator,
|
||||||
} from "@lysand-org/federation";
|
} from "@versia/federation";
|
||||||
import type { Entity } from "@lysand-org/federation/types";
|
import type { Entity } from "@versia/federation/types";
|
||||||
import type { SocketAddress } from "bun";
|
import type { SocketAddress } from "bun";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { matches } from "ip-matching";
|
import { matches } from "ip-matching";
|
||||||
|
|
@ -39,8 +39,9 @@ export const schemas = {
|
||||||
uuid: z.string().uuid(),
|
uuid: z.string().uuid(),
|
||||||
}),
|
}),
|
||||||
header: z.object({
|
header: z.object({
|
||||||
signature: z.string(),
|
"X-Signature": z.string(),
|
||||||
date: z.string(),
|
"X-Nonce": z.string(),
|
||||||
|
"X-Signed-By": z.string().url().or(z.literal("instance")),
|
||||||
authorization: z.string().optional(),
|
authorization: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
body: z.any(),
|
body: z.any(),
|
||||||
|
|
@ -55,8 +56,12 @@ export default apiRoute((app) =>
|
||||||
zValidator("json", schemas.body, handleZodError),
|
zValidator("json", schemas.body, handleZodError),
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const { uuid } = context.req.valid("param");
|
const { uuid } = context.req.valid("param");
|
||||||
const { signature, date, authorization } =
|
const {
|
||||||
context.req.valid("header");
|
"X-Signature": signature,
|
||||||
|
"X-Nonce": nonce,
|
||||||
|
"X-Signed-By": signedBy,
|
||||||
|
authorization,
|
||||||
|
} = context.req.valid("header");
|
||||||
const logger = getLogger(["federation", "inbox"]);
|
const logger = getLogger(["federation", "inbox"]);
|
||||||
|
|
||||||
const body: Entity = await context.req.valid("json");
|
const body: Entity = await context.req.valid("json");
|
||||||
|
|
@ -128,19 +133,24 @@ export default apiRoute((app) =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyId = signature
|
const sender = await User.resolve(signedBy);
|
||||||
.split("keyId=")[1]
|
|
||||||
.split(",")[0]
|
|
||||||
.replace(/"/g, "");
|
|
||||||
const sender = await User.resolve(keyId);
|
|
||||||
|
|
||||||
const origin = new URL(keyId).origin;
|
if (sender?.isLocal()) {
|
||||||
|
return context.json(
|
||||||
|
{ error: "Cannot send federation requests to local users" },
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = new URL(sender?.data.instance?.baseUrl ?? "")
|
||||||
|
.hostname;
|
||||||
|
|
||||||
// Check if Origin is defederated
|
// Check if Origin is defederated
|
||||||
if (
|
if (
|
||||||
config.federation.blocked.find(
|
config.federation.blocked.find(
|
||||||
(blocked) =>
|
(blocked) =>
|
||||||
blocked.includes(origin) || origin.includes(blocked),
|
blocked.includes(hostname) ||
|
||||||
|
hostname.includes(blocked),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Pretend to accept request
|
// Pretend to accept request
|
||||||
|
|
@ -151,7 +161,7 @@ export default apiRoute((app) =>
|
||||||
if (checkSignature) {
|
if (checkSignature) {
|
||||||
if (!sender) {
|
if (!sender) {
|
||||||
return context.json(
|
return context.json(
|
||||||
{ error: "Could not resolve keyId" },
|
{ error: "Could not resolve sender" },
|
||||||
400,
|
400,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -165,23 +175,13 @@ export default apiRoute((app) =>
|
||||||
sender.data.publicKey,
|
sender.data.publicKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
// If base_url uses https and request uses http, rewrite request to use https
|
|
||||||
// This fixes reverse proxy errors
|
|
||||||
const reqUrl = new URL(context.req.url);
|
|
||||||
if (
|
|
||||||
new URL(config.http.base_url).protocol === "https:" &&
|
|
||||||
reqUrl.protocol === "http:"
|
|
||||||
) {
|
|
||||||
reqUrl.protocol = "https:";
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = await validator
|
const isValid = await validator
|
||||||
.validate(
|
.validate(
|
||||||
new Request(reqUrl, {
|
new Request(context.req.url, {
|
||||||
method: context.req.method,
|
method: context.req.method,
|
||||||
headers: {
|
headers: {
|
||||||
Signature: signature,
|
"X-Signature": signature,
|
||||||
Date: date,
|
"X-Date": nonce,
|
||||||
},
|
},
|
||||||
body: await context.req.text(),
|
body: await context.req.text(),
|
||||||
}),
|
}),
|
||||||
|
|
@ -193,7 +193,7 @@ export default apiRoute((app) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
return context.json({ error: "Invalid signature" }, 400);
|
return context.json({ error: "Invalid signature" }, 401);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -332,12 +332,13 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
return response("Follow request rejected", 200);
|
return response("Follow request rejected", 200);
|
||||||
},
|
},
|
||||||
undo: async (undo) => {
|
// "delete" is a reserved keyword in JS
|
||||||
|
delete: async (delete_) => {
|
||||||
// Delete the specified object from database, if it exists and belongs to the user
|
// Delete the specified object from database, if it exists and belongs to the user
|
||||||
const toDelete = undo.object;
|
const toDelete = delete_.target;
|
||||||
|
|
||||||
// Try and find a follow, note, or user with the given URI
|
switch (delete_.deleted_type) {
|
||||||
// Note
|
case "Note": {
|
||||||
const note = await Note.fromSql(
|
const note = await Note.fromSql(
|
||||||
eq(Notes.uri, toDelete),
|
eq(Notes.uri, toDelete),
|
||||||
eq(Notes.authorId, user.id),
|
eq(Notes.authorId, user.id),
|
||||||
|
|
@ -348,10 +349,9 @@ export default apiRoute((app) =>
|
||||||
return response("Note deleted", 200);
|
return response("Note deleted", 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Follow (unfollow/cancel follow request)
|
break;
|
||||||
// TODO: Remember to store URIs of follow requests/objects in the future
|
}
|
||||||
|
case "User": {
|
||||||
// User
|
|
||||||
const otherUser = await User.resolve(toDelete);
|
const otherUser = await User.resolve(toDelete);
|
||||||
|
|
||||||
if (otherUser) {
|
if (otherUser) {
|
||||||
|
|
@ -368,12 +368,17 @@ export default apiRoute((app) =>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
return context.json(
|
return context.json(
|
||||||
{
|
{
|
||||||
error: `Deletetion of object ${toDelete} not implemented`,
|
error: `Deletetion of object ${toDelete} not implemented`,
|
||||||
},
|
},
|
||||||
400,
|
400,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
user: async (user) => {
|
user: async (user) => {
|
||||||
// Refetch user to ensure we have the latest data
|
// Refetch user to ensure we have the latest data
|
||||||
|
|
@ -390,27 +395,6 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
return response("User refreshed", 200);
|
return response("User refreshed", 200);
|
||||||
},
|
},
|
||||||
patch: async (patch) => {
|
|
||||||
// Update the specified note in the database, if it exists and belongs to the user
|
|
||||||
const toPatch = patch.patched_id;
|
|
||||||
|
|
||||||
const note = await Note.fromSql(
|
|
||||||
eq(Notes.uri, toPatch),
|
|
||||||
eq(Notes.authorId, user.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Refetch note
|
|
||||||
if (!note) {
|
|
||||||
return context.json(
|
|
||||||
{ error: "Note not found" },
|
|
||||||
404,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await note.updateFromRemote();
|
|
||||||
|
|
||||||
return response("Note updated", 200);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { apiRoute, applyConfig } from "@/api";
|
import { apiRoute, applyConfig } from "@/api";
|
||||||
import { urlToContentFormat } from "@/content_types";
|
import { urlToContentFormat } from "@/content_types";
|
||||||
import type { ServerMetadata } from "@lysand-org/federation/types";
|
import type { InstanceMetadata } from "@versia/federation/types";
|
||||||
import pkg from "~/package.json";
|
import pkg from "~/package.json";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/packages/config-manager";
|
||||||
|
|
||||||
|
|
@ -19,14 +19,30 @@ export const meta = applyConfig({
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.on(meta.allowedMethods, meta.route, (context) => {
|
app.on(meta.allowedMethods, meta.route, (context) => {
|
||||||
return context.json({
|
return context.json({
|
||||||
type: "ServerMetadata",
|
type: "InstanceMetadata",
|
||||||
|
compatibility: {
|
||||||
|
extensions: ["pub.versia:custom_emojis"],
|
||||||
|
versions: ["0.3.1", "0.4.0"],
|
||||||
|
},
|
||||||
|
host: new URL(config.http.base_url).host,
|
||||||
name: config.instance.name,
|
name: config.instance.name,
|
||||||
|
description: {
|
||||||
|
"text/plain": {
|
||||||
|
content: config.instance.description,
|
||||||
|
remote: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
public_key: {
|
||||||
|
key: config.instance.keys.public,
|
||||||
|
algorithm: "ed25519",
|
||||||
|
},
|
||||||
|
software: {
|
||||||
|
name: "Versia Server",
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
description: config.instance.description,
|
},
|
||||||
logo: urlToContentFormat(config.instance.logo) ?? undefined,
|
banner: urlToContentFormat(config.instance.banner),
|
||||||
banner: urlToContentFormat(config.instance.banner) ?? undefined,
|
logo: urlToContentFormat(config.instance.logo),
|
||||||
supported_extensions: ["org.lysand:custom_emojis"],
|
created_at: "2021-10-01T00:00:00Z",
|
||||||
website: "https://versia.pub",
|
} satisfies InstanceMetadata);
|
||||||
} satisfies ServerMetadata);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { getLogger } from "@logtape/logtape";
|
||||||
import type { ResponseError } from "@lysand-org/federation";
|
import type { ResponseError } from "@versia/federation";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { lookup } from "mime-types";
|
import { lookup } from "mime-types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
|
||||||
22
types/api.ts
22
types/api.ts
|
|
@ -1,5 +1,16 @@
|
||||||
import type { Hono } from "@hono/hono";
|
import type { Hono } from "@hono/hono";
|
||||||
import type { RouterRoute } from "@hono/hono/types";
|
import type { RouterRoute } from "@hono/hono/types";
|
||||||
|
import type {
|
||||||
|
Delete,
|
||||||
|
Follow,
|
||||||
|
FollowAccept,
|
||||||
|
FollowReject,
|
||||||
|
InstanceMetadata,
|
||||||
|
LikeExtension,
|
||||||
|
Note,
|
||||||
|
Unfollow,
|
||||||
|
User,
|
||||||
|
} from "@versia/federation/types";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import type { RolePermissions } from "~/drizzle/schema";
|
import type { RolePermissions } from "~/drizzle/schema";
|
||||||
|
|
||||||
|
|
@ -40,3 +51,14 @@ export interface ApiRouteExports {
|
||||||
};
|
};
|
||||||
default: (app: Hono) => RouterRoute;
|
default: (app: Hono) => RouterRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type KnownEntity =
|
||||||
|
| Note
|
||||||
|
| InstanceMetadata
|
||||||
|
| User
|
||||||
|
| Follow
|
||||||
|
| FollowAccept
|
||||||
|
| FollowReject
|
||||||
|
| Unfollow
|
||||||
|
| Delete
|
||||||
|
| LikeExtension;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ContentFormat } from "@lysand-org/federation/types";
|
import type { ContentFormat } from "@versia/federation/types";
|
||||||
import { lookup } from "mime-types";
|
import { lookup } from "mime-types";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/packages/config-manager";
|
||||||
|
|
||||||
|
|
@ -31,6 +31,7 @@ export const urlToContentFormat = (url?: string): ContentFormat | null => {
|
||||||
return {
|
return {
|
||||||
"image/svg+xml": {
|
"image/svg+xml": {
|
||||||
content: url,
|
content: url,
|
||||||
|
remote: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -41,6 +42,7 @@ export const urlToContentFormat = (url?: string): ContentFormat | null => {
|
||||||
return {
|
return {
|
||||||
[mimeType]: {
|
[mimeType]: {
|
||||||
content: url,
|
content: url,
|
||||||
|
remote: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue