diff --git a/bun.lockb b/bun.lockb index 7d6c614c..36d6e366 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/classes/functions/federation.ts b/classes/functions/federation.ts index a733b2a0..9aa4b666 100644 --- a/classes/functions/federation.ts +++ b/classes/functions/federation.ts @@ -1,15 +1,16 @@ -import type { Undo } from "@lysand-org/federation/types"; -import { config } from "~/packages/config-manager/index"; +import type { Unfollow } from "@versia/federation/types"; 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(); return { - type: "Undo", + type: "Unfollow", id, - author: undoer.getUri(), + author: unfollower.getUri(), created_at: new Date().toISOString(), - object: uri, - uri: new URL(`/undos/${id}`, config.http.base_url).toString(), + followee: unfollowing.getUri(), }; }; diff --git a/classes/functions/like.ts b/classes/functions/like.ts index bd1027ac..7fc34213 100644 --- a/classes/functions/like.ts +++ b/classes/functions/like.ts @@ -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 { db } from "~/drizzle/db"; import { Likes, Notifications } from "~/drizzle/schema"; @@ -11,15 +11,15 @@ export type LikeType = InferSelectModel; /** * Represents a Like entity in the database. */ -export const likeToVersia = (like: LikeType): Like => { +export const likeToVersia = (like: LikeType): LikeExtension => { return { id: like.id, // biome-ignore lint/suspicious/noExplicitAny: to be rewritten author: (like as any).liker?.uri, - type: "Like", + type: "pub.versia:likes/Like", created_at: new Date(like.createdAt).toISOString(), // 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(), }; }; diff --git a/classes/functions/status.ts b/classes/functions/status.ts index d48f6990..d0b881c5 100644 --- a/classes/functions/status.ts +++ b/classes/functions/status.ts @@ -1,7 +1,7 @@ import { mentionValidator } from "@/api"; import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization"; import markdownItTaskLists from "@hackmd/markdown-it-task-lists"; -import type { ContentFormat } from "@lysand-org/federation/types"; +import type { ContentFormat } from "@versia/federation/types"; import { type InferSelectModel, and, diff --git a/classes/functions/user.ts b/classes/functions/user.ts index 271301f7..c2885a1a 100644 --- a/classes/functions/user.ts +++ b/classes/functions/user.ts @@ -2,7 +2,7 @@ import type { Follow, FollowAccept, FollowReject, -} from "@lysand-org/federation/types"; +} from "@versia/federation/types"; import { type InferSelectModel, eq, sql } from "drizzle-orm"; import { db } from "~/drizzle/db"; import { @@ -12,7 +12,6 @@ import { Tokens, type Users, } from "~/drizzle/schema"; -import { config } from "~/packages/config-manager/index"; import type { EmojiWithInstance } from "~/packages/database-interface/emoji"; import { User } from "~/packages/database-interface/user"; import type { Application } from "./application"; @@ -282,7 +281,6 @@ export const followRequestToVersia = ( author: follower.getUri(), followee: followee.getUri(), 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(), created_at: new Date().toISOString(), follower: follower.getUri(), - uri: new URL(`/follows/${id}`, config.http.base_url).toString(), }; }; diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 0d47e19c..73c81a67 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -1,5 +1,5 @@ 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 { relations, sql } from "drizzle-orm"; import { @@ -381,9 +381,9 @@ export const Users = pgTable( }[] >(), endpoints: jsonb("endpoints").$type { 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 { size: value.size || undefined, width: value.width || undefined, sha256: value.hash?.sha256 || undefined, - blurhash: value.blurhash || undefined, + // blurhash: value.blurhash || undefined, }); } } diff --git a/packages/database-interface/emoji.ts b/packages/database-interface/emoji.ts index 55309bbd..0bec1a75 100644 --- a/packages/database-interface/emoji.ts +++ b/packages/database-interface/emoji.ts @@ -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 { [this.data.contentType]: { content: this.data.url, description: this.data.alt || undefined, + remote: true, }, }, }; diff --git a/packages/database-interface/instance.ts b/packages/database-interface/instance.ts index 2ff0319b..2f9e754a 100644 --- a/packages/database-interface/instance.ts +++ b/packages/database-interface/instance.ts @@ -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 { } 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 { } try { - const metadata = await new EntityValidator().ServerMetadata( + const metadata = await new EntityValidator().InstanceMetadata( data, ); @@ -188,7 +188,7 @@ export class Instance extends BaseInterface { private static async fetchActivityPubMetadata( url: string, - ): Promise { + ): Promise { const origin = new URL(url).origin; const wellKnownUrl = new URL("/.well-known/nodeinfo", origin); @@ -285,14 +285,30 @@ export class Instance extends BaseInterface { 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 { return Instance.insert({ baseUrl: host, name: metadata.name, - version: metadata.version, + version: metadata.software.version, logo: metadata.logo, protocol: protocol, }); diff --git a/packages/database-interface/note.ts b/packages/database-interface/note.ts index 707f320f..920bbdc4 100644 --- a/packages/database-interface/note.ts +++ b/packages/database-interface/note.ts @@ -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 { } } + 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 { ).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 { 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 { 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) => diff --git a/packages/database-interface/user.ts b/packages/database-interface/user.ts index 0122891c..1d515a38 100644 --- a/packages/database-interface/user.ts +++ b/packages/database-interface/user.ts @@ -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 { 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 { 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 { 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 { : "", 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 { * @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 { * * @param entity Entity to federate */ - async federateToFollowers(entity: Entity): Promise { + async federateToFollowers(entity: KnownEntity): Promise { // Get followers const followers = await User.manyFromSql( and( @@ -793,7 +789,10 @@ export class User extends BaseInterface { * @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 { 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 { `/users/${user.id}`, config.http.base_url, ).toString(), - public_key: user.publicKey, + key: user.publicKey, + algorithm: "ed25519", }, extensions: { "org.lysand:custom_emojis": { diff --git a/server/api/api/v1/accounts/update_credentials/index.ts b/server/api/api/v1/accounts/update_credentials/index.ts index c386a80b..c99f2ffa 100644 --- a/server/api/api/v1/accounts/update_credentials/index.ts +++ b/server/api/api/v1/accounts/update_credentials/index.ts @@ -168,6 +168,7 @@ export default apiRoute((app) => self.note = await contentToHtml({ "text/markdown": { content: note, + remote: false, }, }); } @@ -235,6 +236,7 @@ export default apiRoute((app) => { "text/markdown": { content: field.name, + remote: false, }, }, undefined, @@ -245,6 +247,7 @@ export default apiRoute((app) => { "text/markdown": { content: field.value, + remote: false, }, }, undefined, @@ -262,11 +265,13 @@ export default apiRoute((app) => key: { "text/html": { content: parsedName, + remote: false, }, }, value: { "text/html": { content: parsedValue, + remote: false, }, }, }); diff --git a/server/api/api/v1/statuses/:id/index.ts b/server/api/api/v1/statuses/:id/index.ts index 5cbc1288..baa6a551 100644 --- a/server/api/api/v1/statuses/:id/index.ts +++ b/server/api/api/v1/statuses/:id/index.ts @@ -9,7 +9,6 @@ import { import { zValidator } from "@hono/zod-validator"; import ISO6391 from "iso-639-1"; import { z } from "zod"; -import { undoFederationRequest } from "~/classes/functions/federation"; import { RolePermissions } from "~/drizzle/schema"; import { config } from "~/packages/config-manager/index"; import { Attachment } from "~/packages/database-interface/attachment"; @@ -136,9 +135,7 @@ export default apiRoute((app) => await note.delete(); - await user.federateToFollowers( - undoFederationRequest(user, note.getUri()), - ); + await user.federateToFollowers(note.deleteToVersia()); return context.json(await note.toApi(user), 200); } @@ -169,6 +166,7 @@ export default apiRoute((app) => ? { [content_type]: { content: statusText, + remote: false, }, } : undefined, diff --git a/server/api/api/v1/statuses/:id/unreblog.ts b/server/api/api/v1/statuses/:id/unreblog.ts index d26563e8..672518dc 100644 --- a/server/api/api/v1/statuses/:id/unreblog.ts +++ b/server/api/api/v1/statuses/:id/unreblog.ts @@ -2,7 +2,6 @@ import { apiRoute, applyConfig, auth, handleZodError } from "@/api"; import { zValidator } from "@hono/zod-validator"; import { and, eq } from "drizzle-orm"; import { z } from "zod"; -import { undoFederationRequest } from "~/classes/functions/federation"; import { Notes, RolePermissions } from "~/drizzle/schema"; import { Note } from "~/packages/database-interface/note"; @@ -63,9 +62,7 @@ export default apiRoute((app) => await existingReblog.delete(); - await user.federateToFollowers( - undoFederationRequest(user, existingReblog.getUri()), - ); + await user.federateToFollowers(existingReblog.deleteToVersia()); const newNote = await Note.fromId(id, user.id); diff --git a/server/api/api/v1/statuses/index.ts b/server/api/api/v1/statuses/index.ts index 3acefb50..209479e2 100644 --- a/server/api/api/v1/statuses/index.ts +++ b/server/api/api/v1/statuses/index.ts @@ -155,6 +155,7 @@ export default apiRoute((app) => content: { [content_type]: { content: status ?? "", + remote: false, }, }, visibility, diff --git a/server/api/objects/:id/index.ts b/server/api/objects/:id/index.ts index ab721d38..011c2853 100644 --- a/server/api/objects/:id/index.ts +++ b/server/api/objects/:id/index.ts @@ -1,7 +1,6 @@ import { apiRoute, applyConfig, handleZodError } from "@/api"; import { response } from "@/response"; import { zValidator } from "@hono/zod-validator"; -import type { Entity } from "@lysand-org/federation/types"; import { and, eq, inArray, sql } from "drizzle-orm"; import { z } from "zod"; import { type LikeType, likeToVersia } from "~/classes/functions/like"; @@ -10,6 +9,7 @@ import { Notes } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; import { Note } from "~/packages/database-interface/note"; import { User } from "~/packages/database-interface/user"; +import type { KnownEntity } from "~/types/api"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -39,7 +39,7 @@ export default apiRoute((app) => let foundObject: Note | LikeType | null = null; let foundAuthor: User | null = null; - let apiObject: Entity | null = null; + let apiObject: KnownEntity | null = null; foundObject = await Note.fromSql( and( diff --git a/server/api/users/:uuid/inbox/index.ts b/server/api/users/:uuid/inbox/index.ts index 55cf5b0b..04cd99f7 100644 --- a/server/api/users/:uuid/inbox/index.ts +++ b/server/api/users/:uuid/inbox/index.ts @@ -7,8 +7,8 @@ import { EntityValidator, RequestParserHandler, SignatureValidator, -} from "@lysand-org/federation"; -import type { Entity } from "@lysand-org/federation/types"; +} from "@versia/federation"; +import type { Entity } from "@versia/federation/types"; import type { SocketAddress } from "bun"; import { eq } from "drizzle-orm"; import { matches } from "ip-matching"; @@ -39,8 +39,9 @@ export const schemas = { uuid: z.string().uuid(), }), header: z.object({ - signature: z.string(), - date: z.string(), + "X-Signature": z.string(), + "X-Nonce": z.string(), + "X-Signed-By": z.string().url().or(z.literal("instance")), authorization: z.string().optional(), }), body: z.any(), @@ -55,8 +56,12 @@ export default apiRoute((app) => zValidator("json", schemas.body, handleZodError), async (context) => { const { uuid } = context.req.valid("param"); - const { signature, date, authorization } = - context.req.valid("header"); + const { + "X-Signature": signature, + "X-Nonce": nonce, + "X-Signed-By": signedBy, + authorization, + } = context.req.valid("header"); const logger = getLogger(["federation", "inbox"]); const body: Entity = await context.req.valid("json"); @@ -128,19 +133,24 @@ export default apiRoute((app) => } } - const keyId = signature - .split("keyId=")[1] - .split(",")[0] - .replace(/"/g, ""); - const sender = await User.resolve(keyId); + const sender = await User.resolve(signedBy); - 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 if ( config.federation.blocked.find( (blocked) => - blocked.includes(origin) || origin.includes(blocked), + blocked.includes(hostname) || + hostname.includes(blocked), ) ) { // Pretend to accept request @@ -151,7 +161,7 @@ export default apiRoute((app) => if (checkSignature) { if (!sender) { return context.json( - { error: "Could not resolve keyId" }, + { error: "Could not resolve sender" }, 400, ); } @@ -165,23 +175,13 @@ export default apiRoute((app) => 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 .validate( - new Request(reqUrl, { + new Request(context.req.url, { method: context.req.method, headers: { - Signature: signature, - Date: date, + "X-Signature": signature, + "X-Date": nonce, }, body: await context.req.text(), }), @@ -193,7 +193,7 @@ export default apiRoute((app) => }); if (!isValid) { - return context.json({ error: "Invalid signature" }, 400); + return context.json({ error: "Invalid signature" }, 401); } } @@ -332,48 +332,53 @@ export default apiRoute((app) => 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 - const toDelete = undo.object; + const toDelete = delete_.target; - // Try and find a follow, note, or user with the given URI - // Note - const note = await Note.fromSql( - eq(Notes.uri, toDelete), - eq(Notes.authorId, user.id), - ); + switch (delete_.deleted_type) { + case "Note": { + const note = await Note.fromSql( + eq(Notes.uri, toDelete), + eq(Notes.authorId, user.id), + ); - if (note) { - await note.delete(); - return response("Note deleted", 200); - } + if (note) { + await note.delete(); + return response("Note deleted", 200); + } - // Follow (unfollow/cancel follow request) - // TODO: Remember to store URIs of follow requests/objects in the future - - // User - const otherUser = await User.resolve(toDelete); - - if (otherUser) { - if (otherUser.id === user.id) { - // Delete own account - await user.delete(); - return response("Account deleted", 200); + break; } - return context.json( - { - error: "Cannot delete other users than self", - }, - 400, - ); - } + case "User": { + const otherUser = await User.resolve(toDelete); - return context.json( - { - error: `Deletetion of object ${toDelete} not implemented`, - }, - 400, - ); + if (otherUser) { + if (otherUser.id === user.id) { + // Delete own account + await user.delete(); + return response("Account deleted", 200); + } + return context.json( + { + error: "Cannot delete other users than self", + }, + 400, + ); + } + + break; + } + default: { + return context.json( + { + error: `Deletetion of object ${toDelete} not implemented`, + }, + 400, + ); + } + } }, user: async (user) => { // Refetch user to ensure we have the latest data @@ -390,27 +395,6 @@ export default apiRoute((app) => 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) { diff --git a/server/api/well-known/versia.ts b/server/api/well-known/versia.ts index 86bddeb6..15af9c8b 100644 --- a/server/api/well-known/versia.ts +++ b/server/api/well-known/versia.ts @@ -1,6 +1,6 @@ import { apiRoute, applyConfig } from "@/api"; 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 { config } from "~/packages/config-manager"; @@ -19,14 +19,30 @@ export const meta = applyConfig({ export default apiRoute((app) => app.on(meta.allowedMethods, meta.route, (context) => { 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, - version: pkg.version, - description: config.instance.description, - logo: urlToContentFormat(config.instance.logo) ?? undefined, - banner: urlToContentFormat(config.instance.banner) ?? undefined, - supported_extensions: ["org.lysand:custom_emojis"], - website: "https://versia.pub", - } satisfies ServerMetadata); + 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, + }, + banner: urlToContentFormat(config.instance.banner), + logo: urlToContentFormat(config.instance.logo), + created_at: "2021-10-01T00:00:00Z", + } satisfies InstanceMetadata); }), ); diff --git a/server/api/well-known/webfinger/index.ts b/server/api/well-known/webfinger/index.ts index 1d5603b1..013d4d92 100644 --- a/server/api/well-known/webfinger/index.ts +++ b/server/api/well-known/webfinger/index.ts @@ -7,7 +7,7 @@ import { } from "@/api"; import { zValidator } from "@hono/zod-validator"; 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 { lookup } from "mime-types"; import { z } from "zod"; diff --git a/types/api.ts b/types/api.ts index 5630dfef..75e023c3 100644 --- a/types/api.ts +++ b/types/api.ts @@ -1,5 +1,16 @@ import type { Hono } from "@hono/hono"; 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 { RolePermissions } from "~/drizzle/schema"; @@ -40,3 +51,14 @@ export interface ApiRouteExports { }; default: (app: Hono) => RouterRoute; } + +export type KnownEntity = + | Note + | InstanceMetadata + | User + | Follow + | FollowAccept + | FollowReject + | Unfollow + | Delete + | LikeExtension; diff --git a/utils/content_types.ts b/utils/content_types.ts index b9be6ebc..c21dc653 100644 --- a/utils/content_types.ts +++ b/utils/content_types.ts @@ -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 { config } from "~/packages/config-manager"; @@ -31,6 +31,7 @@ export const urlToContentFormat = (url?: string): ContentFormat | null => { return { "image/svg+xml": { content: url, + remote: true, }, }; } @@ -41,6 +42,7 @@ export const urlToContentFormat = (url?: string): ContentFormat | null => { return { [mimeType]: { content: url, + remote: true, }, }; };