From 77a675afe6e3c70b59bd412f78a2fa69ecd78e70 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Fri, 3 Nov 2023 17:34:31 -1000 Subject: [PATCH] More work on converting to the Lysand protocol --- bun.lockb | Bin 454748 -> 459724 bytes config/config.example.toml | 8 + database/entities/Emoji.ts | 72 +++++ database/entities/Instance.ts | 161 ++------- database/entities/Like.ts | 13 + database/entities/Object.ts | 92 ++++++ database/entities/Status.ts | 114 +++++-- database/entities/User.ts | 194 ++++++++++- package.json | 2 + server/api/.well-known/lysand.ts | 50 +++ server/api/api/v1/accounts/[id]/note.ts | 2 - server/api/api/v1/accounts/[id]/statuses.ts | 93 +++++- .../v1/accounts/update_credentials/index.ts | 19 ++ server/api/users/[username]/actor/index.ts | 153 --------- server/api/users/[username]/inbox/index.ts | 305 ------------------ server/api/users/[username]/outbox/index.ts | 163 ---------- server/api/users/uuid/inbox/index.ts | 224 +++++++++++++ server/api/users/uuid/index.ts | 44 +++ server/api/users/uuid/outbox/index.ts | 67 ++++ types/lysand/Extension.ts | 6 + types/lysand/Object.ts | 164 ++++++++++ .../extensions/org.lysand/custom_emojis.ts | 7 + types/lysand/extensions/org.lysand/polls.ts | 14 + .../lysand/extensions/org.lysand/reactions.ts | 8 + utils/config.ts | 13 + 25 files changed, 1181 insertions(+), 807 deletions(-) create mode 100644 database/entities/Object.ts create mode 100644 server/api/.well-known/lysand.ts delete mode 100644 server/api/users/[username]/actor/index.ts delete mode 100644 server/api/users/[username]/inbox/index.ts delete mode 100644 server/api/users/[username]/outbox/index.ts create mode 100644 server/api/users/uuid/inbox/index.ts create mode 100644 server/api/users/uuid/index.ts create mode 100644 server/api/users/uuid/outbox/index.ts create mode 100644 types/lysand/Extension.ts create mode 100644 types/lysand/Object.ts create mode 100644 types/lysand/extensions/org.lysand/custom_emojis.ts create mode 100644 types/lysand/extensions/org.lysand/polls.ts create mode 100644 types/lysand/extensions/org.lysand/reactions.ts diff --git a/bun.lockb b/bun.lockb index 6fc6a713ba25df1a257948b009b75e1e88f13072..9fb7e5c1a7639b7e416302af61029f4f80d7e6a2 100755 GIT binary patch delta 91390 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 aF_Me8##gg@Ix;%W-}s&HvKDcj^uGWul_72b delta 88305 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 diff --git a/config/config.example.toml b/config/config.example.toml index 081ca89f..33b13faf 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -128,6 +128,14 @@ remove_media = [] # NOT IMPLEMENTED # significantly depending on processing power) authorized_fetch = false +[instance] +name = "Lysand" +description = "A test instance of Lysand" +# URL to your instance logo (jpg files should be renamed to jpeg) +logo = "" +# URL to your instance banner (jpg files should be renamed to jpeg) +banner = "" + [filters] # Drop notes with these regex filters (only applies to new activities) diff --git a/database/entities/Emoji.ts b/database/entities/Emoji.ts index af8c5a50..45d3e5ef 100644 --- a/database/entities/Emoji.ts +++ b/database/entities/Emoji.ts @@ -2,11 +2,13 @@ import { BaseEntity, Column, Entity, + IsNull, ManyToOne, PrimaryGeneratedColumn, } from "typeorm"; import { APIEmoji } from "~types/entities/emoji"; import { Instance } from "./Instance"; +import { Emoji as LysandEmoji } from "~types/lysand/extensions/org.lysand/custom_emojis"; /** * Represents an emoji entity in the database. @@ -42,12 +44,69 @@ export class Emoji extends BaseEntity { @Column("varchar") url!: string; + /** + * The alt text for the emoji. + */ + @Column("varchar", { + nullable: true, + }) + alt!: string | null; + + /** + * The content type of the emoji. + */ + @Column("varchar") + content_type!: string; + /** * Whether the emoji is visible in the picker. */ @Column("boolean") visible_in_picker!: boolean; + /** + * Used for parsing emojis from local text + * @param text The text to parse + * @returns An array of emojis + */ + static async parseEmojis(text: string): Promise { + const regex = /:[a-zA-Z0-9_]+:/g; + const matches = text.match(regex); + if (!matches) return []; + return ( + await Promise.all( + matches.map(match => + Emoji.findOne({ + where: { + shortcode: match.slice(1, -1), + instance: IsNull(), + }, + relations: ["instance"], + }) + ) + ) + ).filter(emoji => emoji !== null) as Emoji[]; + } + + static async addIfNotExists(emoji: LysandEmoji) { + const existingEmoji = await Emoji.findOne({ + where: { + shortcode: emoji.name, + instance: IsNull(), + }, + }); + if (existingEmoji) return existingEmoji; + const newEmoji = new Emoji(); + newEmoji.shortcode = emoji.name; + // TODO: Content types + newEmoji.url = emoji.url[0].content; + newEmoji.alt = emoji.alt || null; + newEmoji.content_type = emoji.url[0].content_type; + newEmoji.visible_in_picker = true; + await newEmoji.save(); + return newEmoji; + } + /** * Converts the emoji to an APIEmoji object. * @returns The APIEmoji object. @@ -62,4 +121,17 @@ export class Emoji extends BaseEntity { category: undefined, }; } + + toLysand(): LysandEmoji { + return { + name: this.shortcode, + url: [ + { + content: this.url, + content_type: this.content_type, + }, + ], + alt: this.alt || undefined, + }; + } } diff --git a/database/entities/Instance.ts b/database/entities/Instance.ts index 145eab9d..36ee42fb 100644 --- a/database/entities/Instance.ts +++ b/database/entities/Instance.ts @@ -1,62 +1,5 @@ import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm"; -import { APIInstance } from "~types/entities/instance"; -import { APIAccount } from "~types/entities/account"; - -export interface NodeInfo { - software: { - name: string; - version: string; - }; - protocols: string[]; - version: string; - services: { - inbound: string[]; - outbound: string[]; - }; - openRegistrations: boolean; - usage: { - users: { - total: number; - activeHalfyear: number; - activeMonth: number; - }; - localPosts: number; - localComments?: number; - remotePosts?: number; - remoteComments?: number; - }; - metadata: Partial<{ - nodeName: string; - nodeDescription: string; - maintainer: { - name: string; - email: string; - }; - langs: string[]; - tosUrl: string; - repositoryUrl: string; - feedbackUrl: string; - disableRegistration: boolean; - disableLocalTimeline: boolean; - disableRecommendedTimeline: boolean; - disableGlobalTimeline: boolean; - emailRequiredForSignup: boolean; - searchFilters: boolean; - postEditing: boolean; - postImports: boolean; - enableHcaptcha: boolean; - enableRecaptcha: boolean; - maxNoteTextLength: number; - maxCaptionTextLength: number; - enableTwitterIntegration: boolean; - enableGithubIntegration: boolean; - enableDiscordIntegration: boolean; - enableEmail: boolean; - enableServiceWorker: boolean; - proxyAccountName: string | null; - themeColor: string; - }>; -} +import { ContentFormat, ServerMetadata } from "~types/lysand/Object"; /** * Represents an instance in the database. @@ -79,18 +22,27 @@ export class Instance extends BaseEntity { base_url!: string; /** - * The configuration of the instance. + * The name of the instance. */ - @Column("jsonb", { - nullable: true, - }) - instance_data?: APIInstance; + @Column("varchar") + name!: string; /** - * Instance nodeinfo data + * The description of the instance. + */ + @Column("varchar") + version!: string; + + /** + * The logo of the instance. */ @Column("jsonb") - nodeinfo!: NodeInfo; + logo?: ContentFormat[]; + + /** + * The banner of the instance. + */ + banner?: ContentFormat[]; /** * Adds an instance to the database if it doesn't already exist. @@ -114,80 +66,25 @@ export class Instance extends BaseEntity { instance.base_url = hostname; // Fetch the instance configuration - const nodeinfo: NodeInfo = await fetch(`${origin}/nodeinfo/2.0`).then( + const metadata = (await fetch(`${origin}/.well-known/lysand`).then( res => res.json() - ); + )) as Partial; - // Try to fetch configuration from Mastodon-compatible instances - if ( - ["firefish", "iceshrimp", "mastodon", "akkoma", "pleroma"].includes( - nodeinfo.software.name - ) - ) { - const instanceData: APIInstance = await fetch( - `${origin}/api/v1/instance` - ).then(res => res.json()); - - instance.instance_data = instanceData; + if (metadata.type !== "ServerMetadata") { + throw new Error("Invalid instance metadata"); } - instance.nodeinfo = nodeinfo; + if (!(metadata.name && metadata.version)) { + throw new Error("Invalid instance metadata"); + } + + instance.name = metadata.name; + instance.version = metadata.version; + instance.logo = metadata.logo; + instance.banner = metadata.banner; await instance.save(); return instance; } - - /** - * Converts the instance to an API instance. - * @returns The API instance. - */ - // eslint-disable-next-line @typescript-eslint/require-await - async toAPI(): Promise { - return { - uri: this.instance_data?.uri || this.base_url, - approval_required: this.instance_data?.approval_required || false, - email: this.instance_data?.email || "", - thumbnail: this.instance_data?.thumbnail || "", - title: this.instance_data?.title || "", - version: this.instance_data?.version || "", - configuration: this.instance_data?.configuration || { - media_attachments: { - image_matrix_limit: 0, - image_size_limit: 0, - supported_mime_types: [], - video_frame_limit: 0, - video_matrix_limit: 0, - video_size_limit: 0, - }, - polls: { - max_characters_per_option: 0, - max_expiration: 0, - max_options: 0, - min_expiration: 0, - }, - statuses: { - characters_reserved_per_url: 0, - max_characters: 0, - max_media_attachments: 0, - }, - }, - contact_account: - this.instance_data?.contact_account || ({} as APIAccount), - description: this.instance_data?.description || "", - invites_enabled: this.instance_data?.invites_enabled || false, - languages: this.instance_data?.languages || [], - registrations: this.instance_data?.registrations || false, - rules: this.instance_data?.rules || [], - stats: { - domain_count: this.instance_data?.stats.domain_count || 0, - status_count: this.instance_data?.stats.status_count || 0, - user_count: this.instance_data?.stats.user_count || 0, - }, - urls: { - streaming_api: this.instance_data?.urls.streaming_api || "", - }, - max_toot_chars: this.instance_data?.max_toot_chars || 0, - }; - } } diff --git a/database/entities/Like.ts b/database/entities/Like.ts index d28531c5..a4bb72d1 100644 --- a/database/entities/Like.ts +++ b/database/entities/Like.ts @@ -7,6 +7,8 @@ import { } from "typeorm"; import { User } from "./User"; import { Status } from "./Status"; +import { Like as LysandLike } from "~types/lysand/Object"; +import { getConfig } from "@config"; /** * Represents a Like entity in the database. @@ -29,4 +31,15 @@ export class Like extends BaseEntity { @CreateDateColumn() created_at!: Date; + + toLysand(): LysandLike { + return { + id: this.id, + author: this.liker.uri, + type: "Like", + created_at: new Date(this.created_at).toISOString(), + object: this.liked.toLysand().uri, + uri: `${getConfig().http.base_url}/actions/${this.id}`, + }; + } } diff --git a/database/entities/Object.ts b/database/entities/Object.ts new file mode 100644 index 00000000..a6d3737a --- /dev/null +++ b/database/entities/Object.ts @@ -0,0 +1,92 @@ +import { + BaseEntity, + Column, + Entity, + ManyToOne, + PrimaryGeneratedColumn, +} from "typeorm"; +import { LysandObjectType } from "~types/lysand/Object"; + +/** + * Represents a Lysand object in the database. + */ +@Entity({ + name: "objects", +}) +export class LysandObject extends BaseEntity { + /** + * The unique identifier for the object. If local, same as `remote_id` + */ + @PrimaryGeneratedColumn("uuid") + id!: string; + + /** + * UUID of the object across the network. If the object is local, same as `id` + */ + remote_id!: string; + + /** + * Any valid Lysand type, such as `Note`, `Like`, `Follow`, etc. + */ + @Column("varchar") + type!: string; + + /** + * Remote URI for the object + * Example: `https://example.com/publications/ef235cc6-d68c-4756-b0df-4e6623c4d51c` + */ + @Column("varchar") + uri!: string; + + @Column("timestamp") + created_at!: string; + + /** + * References an Actor object by URI + */ + @ManyToOne(() => LysandObject, object => object.uri, { + nullable: true, + }) + author!: LysandObject; + + @Column("jsonb") + extra_data!: Omit< + Omit, "id">, "uri">, + "type" + >; + + @Column("jsonb") + extensions!: Record; + + static new(type: string, uri: string): LysandObject { + const object = new LysandObject(); + object.type = type; + object.uri = uri; + object.created_at = new Date().toISOString(); + return object; + } + + isPublication(): boolean { + return this.type === "Note" || this.type === "Patch"; + } + + isAction(): boolean { + return [ + "Like", + "Follow", + "Dislike", + "FollowAccept", + "FollowReject", + "Undo", + "Announce", + ].includes(this.type); + } + + isActor(): boolean { + return this.type === "User"; + } + + isExtension(): boolean { + return this.type === "Extension"; + } +} diff --git a/database/entities/Status.ts b/database/entities/Status.ts index ab595371..170e5ad4 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -21,6 +21,8 @@ import { Emoji } from "./Emoji"; import { Instance } from "./Instance"; import { Like } from "./Like"; import { AppDataSource } from "~database/datasource"; +import { Note } from "~types/lysand/Object"; +import { htmlToText } from "html-to-text"; const config = getConfig(); @@ -95,6 +97,14 @@ export class Status extends BaseEntity { }) content!: string; + /** + * The content type of this status. + */ + @Column("varchar", { + default: "text/plain", + }) + content_type!: string; + /** * The visibility of this status. */ @@ -289,6 +299,8 @@ export class Status extends BaseEntity { sensitive: boolean; spoiler_text: string; emojis: Emoji[]; + content_type?: string; + mentions?: User[]; reply?: { status: Status; user: User; @@ -299,6 +311,7 @@ export class Status extends BaseEntity { newStatus.account = data.account; newStatus.application = data.application ?? null; newStatus.content = data.content; + newStatus.content_type = data.content_type ?? "text/plain"; newStatus.visibility = data.visibility; newStatus.sensitive = data.sensitive; newStatus.spoiler_text = data.spoiler_text; @@ -318,40 +331,44 @@ export class Status extends BaseEntity { }); // Get list of mentioned users - await Promise.all( - mentionedPeople.map(async person => { - // Check if post is in format @username or @username@instance.com - // If is @username, the user is a local user - const instanceUrl = - person.split("@").length === 3 - ? person.split("@")[2] - : null; + if (!data.mentions) { + await Promise.all( + mentionedPeople.map(async person => { + // Check if post is in format @username or @username@instance.com + // If is @username, the user is a local user + const instanceUrl = + person.split("@").length === 3 + ? person.split("@")[2] + : null; - if (instanceUrl) { - const user = await User.findOne({ - where: { - username: person.split("@")[1], - // If contains instanceUrl - instance: { - base_url: instanceUrl, + if (instanceUrl) { + const user = await User.findOne({ + where: { + username: person.split("@")[1], + // If contains instanceUrl + instance: { + base_url: instanceUrl, + }, }, - }, - relations: userRelations, - }); + relations: userRelations, + }); - newStatus.mentions.push(user as User); - } else { - const user = await User.findOne({ - where: { - username: person.split("@")[1], - }, - relations: userRelations, - }); + newStatus.mentions.push(user as User); + } else { + const user = await User.findOne({ + where: { + username: person.split("@")[1], + }, + relations: userRelations, + }); - newStatus.mentions.push(user as User); - } - }) - ); + newStatus.mentions.push(user as User); + } + }) + ); + } else { + newStatus.mentions = data.mentions; + } await newStatus.save(); return newStatus; @@ -442,4 +459,41 @@ export class Status extends BaseEntity { quote_id: undefined, }; } + + toLysand(): Note { + return { + type: "Note", + created_at: new Date(this.created_at).toISOString(), + id: this.id, + author: this.account.uri, + uri: `${config.http.base_url}/users/${this.account.id}/statuses/${this.id}`, + contents: [ + { + content: this.content, + content_type: "text/html", + }, + { + // Content converted to plaintext + content: htmlToText(this.content), + content_type: "text/plain", + }, + ], + // TODO: Add attachments + attachments: [], + is_sensitive: this.sensitive, + mentions: this.mentions.map(mention => mention.id), + // TODO: Add quotes + quotes: [], + replies_to: this.in_reply_to_post?.id + ? [this.in_reply_to_post.id] + : [], + subject: this.spoiler_text, + extensions: { + "org.lysand:custom_emojis": { + emojis: this.emojis.map(emoji => emoji.toLysand()), + }, + // TODO: Add polls and reactions + }, + }; + } } diff --git a/database/entities/User.ts b/database/entities/User.ts index abf4aab2..f4566c82 100644 --- a/database/entities/User.ts +++ b/database/entities/User.ts @@ -18,6 +18,9 @@ import { Status, statusRelations } from "./Status"; import { APISource } from "~types/entities/source"; import { Relationship } from "./Relationship"; import { Instance } from "./Instance"; +import { User as LysandUser } from "~types/lysand/Object"; +import { htmlToText } from "html-to-text"; +import { Emoji } from "./Emoji"; export const userRelations = ["relationships", "pinned_notes", "instance"]; @@ -35,6 +38,12 @@ export class User extends BaseEntity { @PrimaryGeneratedColumn("uuid") id!: string; + /** + * The user URI on the global network + */ + @Column("varchar") + uri!: string; + /** * The username for the user. */ @@ -82,6 +91,19 @@ export class User extends BaseEntity { }) is_admin!: boolean; + @Column("jsonb", { + nullable: true, + }) + endpoints!: { + liked: string; + disliked: string; + featured: string; + followers: string; + following: string; + inbox: string; + outbox: string; + } | null; + /** * The source for the user. */ @@ -147,6 +169,13 @@ export class User extends BaseEntity { @JoinTable() pinned_notes!: Status[]; + /** + * The emojis for the user. + */ + @ManyToMany(() => Emoji, emoji => emoji.id) + @JoinTable() + emojis!: Emoji[]; + /** * Get the user's avatar in raw URL format * @param config The config to use @@ -180,6 +209,76 @@ export class User extends BaseEntity { return { user: await User.retrieveFromToken(token), token }; } + static async fetchRemoteUser(uri: string) { + const response = await fetch(uri, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + }); + + const data = (await response.json()) as Partial; + + const user = new User(); + + if ( + !( + data.id && + data.username && + data.uri && + data.created_at && + data.disliked && + data.featured && + data.liked && + data.followers && + data.following && + data.inbox && + data.outbox && + data.public_key + ) + ) { + throw new Error("Invalid user data"); + } + + user.id = data.id; + user.username = data.username; + user.uri = data.uri; + user.created_at = new Date(data.created_at); + user.endpoints = { + disliked: data.disliked, + featured: data.featured, + liked: data.liked, + followers: data.followers, + following: data.following, + inbox: data.inbox, + outbox: data.outbox, + }; + + user.avatar = (data.avatar && data.avatar[0].content) || ""; + user.header = (data.header && data.header[0].content) || ""; + user.display_name = data.display_name ?? ""; + // TODO: Add bio content types + user.note = data.bio?.[0].content ?? ""; + + // Parse emojis and add them to database + const emojis = + data.extensions?.["org.lysand:custom_emojis"]?.emojis ?? []; + + for (const emoji of emojis) { + user.emojis.push(await Emoji.addIfNotExists(emoji)); + } + + user.public_key = data.public_key.public_key; + + const uriData = new URL(data.uri); + + user.instance = await Instance.addIfNotExists(uriData.origin); + + await user.save(); + return user; + } + /** * Fetches the list of followers associated with the actor and updates the user's followers */ @@ -229,6 +328,7 @@ export class User extends BaseEntity { user.note = data.bio ?? ""; user.avatar = data.avatar ?? config.defaults.avatar; user.header = data.header ?? config.defaults.avatar; + user.uri = `${config.http.base_url}/users/${user.id}`; user.relationships = []; user.instance = null; @@ -348,15 +448,10 @@ export class User extends BaseEntity { * Generates keys for the user. */ async generateKeys(): Promise { - // openssl genrsa -out private.pem 2048 - // openssl rsa -in private.pem -outform PEM -pubout -out public.pem - const keys = await crypto.subtle.generateKey( { - name: "RSASSA-PKCS1-v1_5", - hash: "SHA-256", - modulusLength: 4096, - publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + name: "ed25519", + namedCurve: "ed25519", }, true, ["sign", "verify"] @@ -366,7 +461,7 @@ export class User extends BaseEntity { String.fromCharCode.apply(null, [ ...new Uint8Array( // jesus help me what do these letters mean - await crypto.subtle.exportKey("pkcs8", keys.privateKey) + await crypto.subtle.exportKey("raw", keys.privateKey) ), ]) ); @@ -374,7 +469,7 @@ export class User extends BaseEntity { String.fromCharCode( ...new Uint8Array( // why is exporting a key so hard - await crypto.subtle.exportKey("spki", keys.publicKey) + await crypto.subtle.exportKey("raw", keys.publicKey) ) ) ); @@ -431,7 +526,7 @@ export class User extends BaseEntity { followers_count: follower_count, following_count: following_count, statuses_count: statusCount, - emojis: [], + emojis: await Promise.all(this.emojis.map(emoji => emoji.toAPI())), fields: [], bot: false, source: isOwnAccount ? this.source : undefined, @@ -451,4 +546,83 @@ export class User extends BaseEntity { role: undefined, }; } + + /** + * Should only return local users + */ + toLysand(): LysandUser { + if (this.instance !== null) { + throw new Error("Cannot convert remote user to Lysand format"); + } + + return { + id: this.id, + type: "User", + uri: this.uri, + bio: [ + { + content: this.note, + content_type: "text/html", + }, + { + content: htmlToText(this.note), + content_type: "text/plain", + }, + ], + created_at: new Date(this.created_at).toISOString(), + disliked: `${this.uri}/disliked`, + featured: `${this.uri}/featured`, + liked: `${this.uri}/liked`, + followers: `${this.uri}/followers`, + following: `${this.uri}/following`, + inbox: `${this.uri}/inbox`, + outbox: `${this.uri}/outbox`, + indexable: false, + username: this.username, + avatar: [ + { + content: this.getAvatarUrl(getConfig()) || "", + content_type: `image/${this.avatar.split(".")[1]}`, + }, + ], + header: [ + { + content: this.getHeaderUrl(getConfig()) || "", + content_type: `image/${this.header.split(".")[1]}`, + }, + ], + display_name: this.display_name, + fields: this.source.fields.map(field => ({ + key: [ + { + content: field.name, + content_type: "text/html", + }, + { + content: htmlToText(field.name), + content_type: "text/plain", + }, + ], + value: [ + { + content: field.value, + content_type: "text/html", + }, + { + content: htmlToText(field.value), + content_type: "text/plain", + }, + ], + })), + public_key: { + actor: `${getConfig().http.base_url}/users/${this.id}`, + public_key: this.public_key, + }, + extensions: { + "org.lysand:custom_emojis": { + emojis: this.emojis.map(emoji => emoji.toLysand()), + }, + }, + }; + } } diff --git a/package.json b/package.json index 0311f2fc..4aca90e9 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ ], "devDependencies": { "@julr/unocss-preset-forms": "^0.0.5", + "@types/html-to-text": "^9.0.3", "@types/jsonld": "^1.5.9", "@typescript-eslint/eslint-plugin": "^6.6.0", "@typescript-eslint/parser": "^6.6.0", @@ -61,6 +62,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.429.0", "chalk": "^5.3.0", + "html-to-text": "^9.0.5", "ip-matching": "^2.1.2", "isomorphic-dompurify": "^1.9.0", "jsonld": "^8.3.1", diff --git a/server/api/.well-known/lysand.ts b/server/api/.well-known/lysand.ts new file mode 100644 index 00000000..dd685ce0 --- /dev/null +++ b/server/api/.well-known/lysand.ts @@ -0,0 +1,50 @@ +import { jsonResponse } from "@response"; +import { MatchedRoute } from "bun"; +import { getConfig } from "@config"; +import { applyConfig } from "@api"; + +export const meta = applyConfig({ + allowedMethods: ["GET"], + auth: { + required: false, + }, + ratelimits: { + duration: 60, + max: 60, + }, + route: "/.well-known/lysand", +}); + +/** + * Lysand instance metadata endpoint + */ +export default async ( + req: Request, + matchedRoute: MatchedRoute +): Promise => { + const config = getConfig(); + // In the format acct:name@example.com + return jsonResponse({ + type: "ServerMetadata", + name: config.instance.name, + version: "0.0.1", + description: config.instance.description, + logo: config.instance.logo ? [ + { + content: config.instance.logo, + content_type: `image/${config.instance.logo.split(".")[1]}`, + } + ] : undefined, + banner: config.instance.banner ? [ + { + content: config.instance.banner, + content_type: `image/${config.instance.banner.split(".")[1]}`, + } + ] : undefined, + supported_extensions: [ + "org.lysand:custom_emojis" + ], + website: "https://lysand.org", + // TODO: Add admins, moderators field + }) +}; diff --git a/server/api/api/v1/accounts/[id]/note.ts b/server/api/api/v1/accounts/[id]/note.ts index db7f76e9..76b1fdea 100644 --- a/server/api/api/v1/accounts/[id]/note.ts +++ b/server/api/api/v1/accounts/[id]/note.ts @@ -59,8 +59,6 @@ export default async ( relationship.note = comment ?? ""; - // TODO: Implement duration - await relationship.save(); return jsonResponse(await relationship.toAPI()); }; diff --git a/server/api/api/v1/accounts/[id]/statuses.ts b/server/api/api/v1/accounts/[id]/statuses.ts index 6d2bbee8..120cfc43 100644 --- a/server/api/api/v1/accounts/[id]/statuses.ts +++ b/server/api/api/v1/accounts/[id]/statuses.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; import { Status, statusAndUserRelations } from "~database/entities/Status"; import { User, userRelations } from "~database/entities/User"; import { applyConfig } from "@api"; +import { FindManyOptions } from "typeorm"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -25,10 +27,13 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; + // TODO: Add pinned const { + max_id, + min_id, + since_id, limit, exclude_reblogs, - pinned, }: { max_id?: string; since_id?: string; @@ -51,12 +56,8 @@ export default async ( if (!user) return errorResponse("User not found", 404); - if (pinned) { - // TODO: Add pinned statuses - } - - // TODO: Check if status can be seen by this user - const statuses = await Status.find({ + // Get list of boosts for this status + let query: FindManyOptions = { where: { account: { id: user.id, @@ -64,13 +65,81 @@ export default async ( isReblog: exclude_reblogs ? true : undefined, }, relations: statusAndUserRelations, - order: { - created_at: "DESC", - }, take: limit ?? 20, - }); + order: { + id: "DESC", + }, + }; + + if (max_id) { + const maxStatus = await Status.findOneBy({ id: max_id }); + if (maxStatus) { + query = { + ...query, + where: { + ...query.where, + created_at: { + ...(query.where as any)?.created_at, + $lt: maxStatus.created_at, + }, + }, + }; + } + } + + if (since_id) { + const sinceStatus = await Status.findOneBy({ id: since_id }); + if (sinceStatus) { + query = { + ...query, + where: { + ...query.where, + created_at: { + ...(query.where as any)?.created_at, + $gt: sinceStatus.created_at, + }, + }, + }; + } + } + + if (min_id) { + const minStatus = await Status.findOneBy({ id: min_id }); + if (minStatus) { + query = { + ...query, + where: { + ...query.where, + created_at: { + ...(query.where as any)?.created_at, + $gte: minStatus.created_at, + }, + }, + }; + } + } + + const objects = await Status.find(query); + + // Constuct HTTP Link header (next and prev) + const linkHeader = []; + if (objects.length > 0) { + const urlWithoutQuery = req.url.split("?")[0]; + linkHeader.push( + `<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"` + ); + linkHeader.push( + `<${urlWithoutQuery}?since_id=${ + objects[objects.length - 1].id + }&limit=${limit}>; rel="prev"` + ); + } return jsonResponse( - await Promise.all(statuses.map(async status => await status.toAPI())) + await Promise.all(objects.map(async status => await status.toAPI())), + 200, + { + Link: linkHeader.join(", "), + } ); }; diff --git a/server/api/api/v1/accounts/update_credentials/index.ts b/server/api/api/v1/accounts/update_credentials/index.ts index a88bb82a..105c6b07 100644 --- a/server/api/api/v1/accounts/update_credentials/index.ts +++ b/server/api/api/v1/accounts/update_credentials/index.ts @@ -6,6 +6,7 @@ import { applyConfig } from "@api"; import { sanitize } from "isomorphic-dompurify"; import { sanitizeHtml } from "@sanitization"; import { uploadFile } from "~classes/media"; +import { Emoji } from "~database/entities/Emoji"; export const meta = applyConfig({ allowedMethods: ["PATCH"], @@ -81,6 +82,9 @@ export default async (req: Request): Promise => { return errorResponse("Display name contains blocked words", 422); } + // Remove emojis + user.emojis = []; + user.display_name = sanitizedDisplayName; } @@ -102,6 +106,9 @@ export default async (req: Request): Promise => { return errorResponse("Bio contains blocked words", 422); } + // Remove emojis + user.emojis = []; + user.note = sanitizedNote; } @@ -193,6 +200,18 @@ export default async (req: Request): Promise => { // user.discoverable = discoverable === "true"; } + // Parse emojis + + const displaynameEmojis = await Emoji.parseEmojis(sanitizedDisplayName); + const noteEmojis = await Emoji.parseEmojis(sanitizedNote); + + user.emojis = [...displaynameEmojis, ...noteEmojis]; + + // Deduplicate emojis + user.emojis = user.emojis.filter( + (emoji, index, self) => self.findIndex(e => e.id === emoji.id) === index + ); + await user.save(); return jsonResponse(await user.toAPI()); diff --git a/server/api/users/[username]/actor/index.ts b/server/api/users/[username]/actor/index.ts deleted file mode 100644 index 669dc0cd..00000000 --- a/server/api/users/[username]/actor/index.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { errorResponse, jsonLdResponse } from "@response"; -import { MatchedRoute } from "bun"; -import { User, userRelations } from "~database/entities/User"; -import { getConfig, getHost } from "@config"; -import { applyConfig } from "@api"; - -export const meta = applyConfig({ - allowedMethods: ["GET"], - auth: { - required: false, - }, - ratelimits: { - duration: 60, - max: 500, - }, - route: "/users/:username/actor", -}); - -/** - * ActivityPub user actor endpoinmt - */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { - // Check for Accept header - const accept = req.headers.get("Accept"); - - if (!accept || !accept.includes("application/activity+json")) { - return errorResponse("This endpoint requires an Accept header", 406); - } - - const config = getConfig(); - - const username = matchedRoute.params.username; - - const user = await User.findOne({ - where: { username }, - relations: userRelations, - }); - - if (!user) { - return errorResponse("User not found", 404); - } - - return jsonLdResponse({ - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - { - manuallyApprovesFollowers: "as:manuallyApprovesFollowers", - toot: "http://joinmastodon.org/ns#", - featured: { - "@id": "toot:featured", - "@type": "@id", - }, - featuredTags: { - "@id": "toot:featuredTags", - "@type": "@id", - }, - alsoKnownAs: { - "@id": "as:alsoKnownAs", - "@type": "@id", - }, - movedTo: { - "@id": "as:movedTo", - "@type": "@id", - }, - schema: "http://schema.org#", - PropertyValue: "schema:PropertyValue", - value: "schema:value", - discoverable: "toot:discoverable", - Device: "toot:Device", - Ed25519Signature: "toot:Ed25519Signature", - Ed25519Key: "toot:Ed25519Key", - Curve25519Key: "toot:Curve25519Key", - EncryptedMessage: "toot:EncryptedMessage", - publicKeyBase64: "toot:publicKeyBase64", - deviceId: "toot:deviceId", - claim: { - "@type": "@id", - "@id": "toot:claim", - }, - fingerprintKey: { - "@type": "@id", - "@id": "toot:fingerprintKey", - }, - identityKey: { - "@type": "@id", - "@id": "toot:identityKey", - }, - devices: { - "@type": "@id", - "@id": "toot:devices", - }, - messageFranking: "toot:messageFranking", - messageType: "toot:messageType", - cipherText: "toot:cipherText", - suspended: "toot:suspended", - Emoji: "toot:Emoji", - focalPoint: { - "@container": "@list", - "@id": "toot:focalPoint", - }, - Hashtag: "as:Hashtag", - }, - ], - id: `${config.http.base_url}/users/${user.username}`, - type: "Person", - preferredUsername: user.username, // TODO: Add user display name - name: user.username, - summary: user.note, - icon: { - type: "Image", - url: user.avatar, - mediaType: "image/png", // TODO: Set user avatar mimetype - }, - image: { - type: "Image", - url: user.header, - mediaType: "image/png", // TODO: Set user header mimetype - }, - inbox: `${config.http.base_url}/users/${user.username}/inbox`, - outbox: `${config.http.base_url}/users/${user.username}/outbox`, - followers: `${config.http.base_url}/users/${user.username}/followers`, - following: `${config.http.base_url}/users/${user.username}/following`, - liked: `${config.http.base_url}/users/${user.username}/liked`, - discoverable: true, - alsoKnownAs: [ - // TODO: Add accounts from which the user migrated - ], - manuallyApprovesFollowers: false, // TODO: Change - publicKey: { - id: `${getHost()}${config.http.base_url}/users/${ - user.username - }/actor#main-key`, - owner: `${config.http.base_url}/users/${user.username}`, - // Split the public key into PEM format - publicKeyPem: `-----BEGIN PUBLIC KEY-----\n${user.public_key - .match(/.{1,64}/g) - ?.join("\n")}\n-----END PUBLIC KEY-----`, - }, - tag: [ - // TODO: Add emojis here, and hashtags - ], - attachment: [ - // TODO: Add user attachments (I.E. profile metadata) - ], - endpoints: { - sharedInbox: `${config.http.base_url}/inbox`, - }, - }); -}; diff --git a/server/api/users/[username]/inbox/index.ts b/server/api/users/[username]/inbox/index.ts deleted file mode 100644 index 0faa8517..00000000 --- a/server/api/users/[username]/inbox/index.ts +++ /dev/null @@ -1,305 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { applyConfig } from "@api"; -import { getConfig } from "@config"; -import { errorResponse, jsonResponse } from "@response"; -import { - APAccept, - APActivity, - APActor, - APCreate, - APDelete, - APFollow, - APObject, - APReject, - APTombstone, - APUpdate, -} from "activitypub-types"; -import { MatchedRoute } from "bun"; -import { RawActivity } from "~database/entities/RawActivity"; -import { RawActor } from "~database/entities/RawActor"; -import { User } from "~database/entities/User"; - -export const meta = applyConfig({ - allowedMethods: ["POST"], - auth: { - required: false, - }, - ratelimits: { - duration: 60, - max: 500, - }, - route: "/users/:username/inbox", -}); - -/** - * ActivityPub user inbox endpoint - */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { - const username = matchedRoute.params.username; - - const config = getConfig(); - - try { - if ( - config.activitypub.reject_activities.includes( - new URL(req.headers.get("Origin") ?? "").hostname - ) - ) { - // Discard request - return jsonResponse({}); - } - } catch (e) { - console.error( - `[-] Error parsing Origin header of incoming Activity from ${req.headers.get( - "Origin" - )}` - ); - console.error(e); - } - - // Process request body - const body: APActivity = await req.json(); - - // Verify HTTP signature - if (config.activitypub.authorized_fetch) { - // Check if date is older than 30 seconds - const date = new Date(req.headers.get("Date") ?? ""); - - if (date.getTime() < Date.now() - 30000) { - return errorResponse("Date is too old (max 30 seconds)", 401); - } - - const signature = req.headers.get("Signature") ?? ""; - const signatureParams = signature - .split(",") - .reduce>((params, param) => { - const [key, value] = param.split("="); - params[key] = value.replace(/"/g, ""); - return params; - }, {}); - - const signedString = `(request-target): post /users/${username}/inbox\nhost: ${ - config.http.base_url - }\ndate: ${req.headers.get("Date")}`; - const signatureBuffer = new TextEncoder().encode( - signatureParams.signature - ); - const signatureBytes = new Uint8Array(signatureBuffer).buffer; - const publicKeyBuffer = (body.actor as any).publicKey.publicKeyPem; - const publicKey = await crypto.subtle.importKey( - "spki", - publicKeyBuffer, - { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, - false, - ["verify"] - ); - const verified = await crypto.subtle.verify( - { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, - publicKey, - signatureBytes, - new TextEncoder().encode(signedString) - ); - - if (!verified) { - return errorResponse("Invalid signature", 401); - } - } - - // Get the object's ActivityPub type - const type = body.type; - - switch (type) { - case "Create" as APCreate: { - // Body is an APCreate object - // Store the Create object in database - // TODO: Add authentication - - // Check is Activity already exists - const activity = await RawActivity.createIfNotExists(body); - - if (activity instanceof Response) { - return activity; - } - break; - } - case "Update" as APUpdate: { - // Body is an APUpdate object - // Replace the object in database with the new provided object - // TODO: Add authentication - - try { - if ( - config.activitypub.discard_updates.includes( - new URL(req.headers.get("Origin") ?? "").hostname - ) - ) { - // Discard request - return jsonResponse({}); - } - } catch (e) { - console.error( - `[-] Error parsing Origin header of incoming Update Activity from ${req.headers.get( - "Origin" - )}` - ); - console.error(e); - } - - const object = await RawActivity.updateObjectIfExists( - body.object as APObject - ); - - if (object instanceof Response) { - return object; - } - - const activity = await RawActivity.createIfNotExists(body, object); - - if (activity instanceof Response) { - return activity; - } - - break; - } - case "Delete" as APDelete: { - // Body is an APDelete object - // Delete the object from database - // TODO: Add authentication - - try { - if ( - config.activitypub.discard_deletes.includes( - new URL(req.headers.get("Origin") ?? "").hostname - ) - ) { - // Discard request - return jsonResponse({}); - } - } catch (e) { - console.error( - `[-] Error parsing Origin header of incoming Delete Activity from ${req.headers.get( - "Origin" - )}` - ); - console.error(e); - } - - const response = await RawActivity.deleteObjectIfExists( - body.object as APObject - ); - - if (response instanceof Response) { - return response; - } - - // Store the Delete event in the database - const activity = await RawActivity.createIfNotExists(body); - - if (activity instanceof Response) { - return activity; - } - break; - } - case "Accept" as APAccept: { - // Body is an APAccept object - // Add the actor to the object actor's followers list - - if ((body.object as APFollow).type === "Follow") { - const user = await User.getByActorId( - ((body.object as APFollow).actor as APActor).id ?? "" - ); - - if (!user) { - return errorResponse("User not found", 404); - } - - const actor = await RawActor.addIfNotExists( - body.actor as APActor - ); - - if (actor instanceof Response) { - return actor; - } - - // TODO: Add follower - - await user.save(); - } - break; - } - case "Reject" as APReject: { - // Body is an APReject object - // Mark the follow request as not pending - - if ((body.object as APFollow).type === "Follow") { - const user = await User.getByActorId( - ((body.object as APFollow).actor as APActor).id ?? "" - ); - - if (!user) { - return errorResponse("User not found", 404); - } - - const actor = await RawActor.addIfNotExists( - body.actor as APActor - ); - - if (actor instanceof Response) { - return actor; - } - - // TODO: Remove follower - - await user.save(); - } - break; - } - case "Follow" as APFollow: { - // Body is an APFollow object - // Add the actor to the object actor's followers list - - try { - if ( - config.activitypub.discard_follows.includes( - new URL(req.headers.get("Origin") ?? "").hostname - ) - ) { - // Reject request - return jsonResponse({}); - } - } catch (e) { - console.error( - `[-] Error parsing Origin header of incoming Delete Activity from ${req.headers.get( - "Origin" - )}` - ); - console.error(e); - } - - const user = await User.getByActorId( - (body.actor as APActor).id ?? "" - ); - - if (!user) { - return errorResponse("User not found", 404); - } - - const actor = await RawActor.addIfNotExists(body.actor as APActor); - - if (actor instanceof Response) { - return actor; - } - - // TODO: Add follower - - await user.save(); - break; - } - } - - return jsonResponse({}); -}; diff --git a/server/api/users/[username]/outbox/index.ts b/server/api/users/[username]/outbox/index.ts deleted file mode 100644 index 3a855163..00000000 --- a/server/api/users/[username]/outbox/index.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { errorResponse, jsonLdResponse } from "@response"; -import { MatchedRoute } from "bun"; -import { User, userRelations } from "~database/entities/User"; -import { getHost } from "@config"; -import { NodeObject, compact } from "jsonld"; -import { RawActivity } from "~database/entities/RawActivity"; -import { applyConfig } from "@api"; - -export const meta = applyConfig({ - allowedMethods: ["GET"], - auth: { - required: false, - }, - ratelimits: { - duration: 60, - max: 500, - }, - route: "/users/:username/outbox", -}); - -/** - * ActivityPub user outbox endpoint - */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { - const username = matchedRoute.params.username.split("@")[0]; - const page = Boolean(matchedRoute.query.page || "false"); - const min_id = matchedRoute.query.min_id || false; - const max_id = matchedRoute.query.max_id || false; - - const user = await User.findOne({ - where: { username }, - relations: userRelations, - }); - - if (!user) { - return errorResponse("User not found", 404); - } - - // Get the user's corresponding ActivityPub notes - const count = await RawActivity.count({ - where: { - data: { - attributedTo: `${getHost()}/@${user.username}`, - }, - }, - order: { - data: { - published: "DESC", - }, - }, - }); - - const lastPost = ( - await RawActivity.find({ - where: { - data: { - attributedTo: `${getHost()}/@${user.username}`, - }, - }, - order: { - data: { - published: "ASC", - }, - }, - take: 1, - }) - )[0]; - - if (!page) - return jsonLdResponse( - await compact({ - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - ], - id: `${getHost()}/@${user.username}/inbox`, - type: "OrderedCollection", - totalItems: count, - first: `${getHost()}/@${user.username}/outbox?page=true`, - last: `${getHost()}/@${user.username}/outbox?min_id=${ - lastPost.id - }&page=true`, - }) - ); - else { - let posts: RawActivity[] = []; - - if (min_id) { - posts = await RawActivity.find({ - where: { - data: { - attributedTo: `${getHost()}/@${user.username}`, - id: min_id, - }, - }, - order: { - data: { - published: "DESC", - }, - }, - take: 11, // Take one extra to have the ID of the next post - }); - } else if (max_id) { - posts = await RawActivity.find({ - where: { - data: { - attributedTo: `${getHost()}/@${user.username}`, - id: max_id, - }, - }, - order: { - data: { - published: "ASC", - }, - }, - take: 10, - }); - } - - return jsonLdResponse( - await compact({ - "@context": [ - "https://www.w3.org/ns/activitystreams", - { - ostatus: "http://ostatus.org#", - atomUri: "ostatus:atomUri", - inReplyToAtomUri: "ostatus:inReplyToAtomUri", - conversation: "ostatus:conversation", - sensitive: "as:sensitive", - toot: "http://joinmastodon.org/ns#", - votersCount: "toot:votersCount", - litepub: "http://litepub.social/ns#", - directMessage: "litepub:directMessage", - Emoji: "toot:Emoji", - focalPoint: { - "@container": "@list", - "@id": "toot:focalPoint", - }, - blurhash: "toot:blurhash", - }, - ], - id: `${getHost()}/@${user.username}/inbox`, - type: "OrderedCollectionPage", - totalItems: count, - partOf: `${getHost()}/@${user.username}/inbox`, - // Next is less recent posts chronologically, uses min_id - next: `${getHost()}/@${user.username}/outbox?min_id=${ - posts[posts.length - 1].id - }&page=true`, - // Prev is more recent posts chronologically, uses max_id - prev: `${getHost()}/@${user.username}/outbox?max_id=${ - posts[0].id - }&page=true`, - orderedItems: posts - .slice(0, 10) - .map(post => post.data) as NodeObject[], - }) - ); - } -}; diff --git a/server/api/users/uuid/inbox/index.ts b/server/api/users/uuid/inbox/index.ts new file mode 100644 index 00000000..bc58339c --- /dev/null +++ b/server/api/users/uuid/inbox/index.ts @@ -0,0 +1,224 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { applyConfig } from "@api"; +import { getConfig } from "@config"; +import { errorResponse, jsonResponse } from "@response"; +import { MatchedRoute } from "bun"; +import { Status } from "~database/entities/Status"; +import { User, userRelations } from "~database/entities/User"; +import { + ContentFormat, + LysandAction, + LysandObjectType, + LysandPublication, +} from "~types/lysand/Object"; + +export const meta = applyConfig({ + allowedMethods: ["POST"], + auth: { + required: false, + }, + ratelimits: { + duration: 60, + max: 500, + }, + route: "/users/:username/inbox", +}); + +/** + * ActivityPub user inbox endpoint + */ +export default async ( + req: Request, + matchedRoute: MatchedRoute +): Promise => { + const username = matchedRoute.params.username; + + const config = getConfig(); + + try { + if ( + config.activitypub.reject_activities.includes( + new URL(req.headers.get("Origin") ?? "").hostname + ) + ) { + // Discard request + return jsonResponse({}); + } + } catch (e) { + console.error( + `[-] Error parsing Origin header of incoming Activity from ${req.headers.get( + "Origin" + )}` + ); + console.error(e); + } + + // Process request body + const body = (await req.json()) as LysandPublication | LysandAction; + + const author = await User.findOne({ + where: { + uri: body.author, + }, + relations: userRelations, + }); + + if (!author) { + // TODO: Add new author to database + return errorResponse("Author not found", 404); + } + + // Verify HTTP signature + if (config.activitypub.authorized_fetch) { + // Check if date is older than 30 seconds + const origin = req.headers.get("Origin"); + + if (!origin) { + return errorResponse("Origin header is required", 401); + } + + const date = req.headers.get("Date"); + + if (!date) { + return errorResponse("Date header is required", 401); + } + + if (new Date(date).getTime() < Date.now() - 30000) { + return errorResponse("Date is too old (max 30 seconds)", 401); + } + + const signatureHeader = req.headers.get("Signature"); + + if (!signatureHeader) { + return errorResponse("Signature header is required", 401); + } + + const signature = signatureHeader + .split("signature=")[1] + .replace(/"/g, ""); + + const digest = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(await req.text()) + ); + + const expectedSignedString = + `(request-target): ${req.method.toLowerCase()} ${req.url}\n` + + `host: ${req.url}\n` + + `date: ${date}\n` + + `digest: SHA-256=${Buffer.from(digest).toString("base64")}`; + + // author.public_key is base64 encoded raw public key + const publicKey = await crypto.subtle.importKey( + "raw", + Buffer.from(author.public_key, "base64"), + { + name: "ed25519", + }, + false, + ["verify"] + ); + + // Check if signed string is valid + const isValid = await crypto.subtle.verify( + { + name: "ed25519", + saltLength: 0, + }, + publicKey, + new TextEncoder().encode(signature), + new TextEncoder().encode(expectedSignedString) + ); + + if (!isValid) { + throw new Error("Invalid signature"); + } + } + + // Get the object's ActivityPub type + const type = body.type; + + switch (type) { + case "Note": { + let content: ContentFormat | null; + + // Find the best content and content type + if ( + body.contents.find( + c => c.content_type === "text/x.misskeymarkdown" + ) + ) { + content = + body.contents.find( + c => c.content_type === "text/x.misskeymarkdown" + ) || null; + } else if ( + body.contents.find(c => c.content_type === "text/html") + ) { + content = + body.contents.find(c => c.content_type === "text/html") || + null; + } else if ( + body.contents.find(c => c.content_type === "text/markdown") + ) { + content = + body.contents.find( + c => c.content_type === "text/markdown" + ) || null; + } else if ( + body.contents.find(c => c.content_type === "text/plain") + ) { + content = + body.contents.find(c => c.content_type === "text/plain") || + null; + } else { + content = body.contents[0] || null; + } + + const status = await Status.createNew({ + account: author, + content: content?.content || "", + content_type: content?.content_type, + application: null, + // TODO: Add visibility + visibility: "public", + spoiler_text: body.subject || "", + sensitive: body.is_sensitive, + // TODO: Add emojis + emojis: [], + }); + + break; + } + case "Patch": { + break; + } + case "Like": { + break; + } + case "Dislike": { + break; + } + case "Follow": { + break; + } + case "FollowAccept": { + break; + } + case "FollowReject": { + break; + } + case "Announce": { + break; + } + case "Undo": { + break; + } + default: { + return errorResponse("Invalid type", 400); + } + } + + return jsonResponse({}); +}; diff --git a/server/api/users/uuid/index.ts b/server/api/users/uuid/index.ts new file mode 100644 index 00000000..8c6bc1c9 --- /dev/null +++ b/server/api/users/uuid/index.ts @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { applyConfig } from "@api"; +import { getConfig } from "@config"; +import { errorResponse, jsonResponse } from "@response"; +import { MatchedRoute } from "bun"; +import { User, userRelations } from "~database/entities/User"; + +export const meta = applyConfig({ + allowedMethods: ["POST"], + auth: { + required: false, + }, + ratelimits: { + duration: 60, + max: 500, + }, + route: "/users/:uuid", +}); + +/** + * ActivityPub user inbox endpoint + */ +export default async ( + req: Request, + matchedRoute: MatchedRoute +): Promise => { + const uuid = matchedRoute.params.uuid; + + const config = getConfig(); + + const user = await User.findOne({ + where: { + id: uuid, + }, + relations: userRelations, + }); + + if (!user) { + return errorResponse("User not found", 404); + } + + return jsonResponse(user.toLysand()); +}; diff --git a/server/api/users/uuid/outbox/index.ts b/server/api/users/uuid/outbox/index.ts new file mode 100644 index 00000000..16a1fa0e --- /dev/null +++ b/server/api/users/uuid/outbox/index.ts @@ -0,0 +1,67 @@ +import { jsonResponse } from "@response"; +import { MatchedRoute } from "bun"; +import { userRelations } from "~database/entities/User"; +import { getHost } from "@config"; +import { applyConfig } from "@api"; +import { Status } from "~database/entities/Status"; +import { In } from "typeorm"; + +export const meta = applyConfig({ + allowedMethods: ["GET"], + auth: { + required: false, + }, + ratelimits: { + duration: 60, + max: 500, + }, + route: "/users/:uuid/outbox", +}); + +/** + * ActivityPub user outbox endpoint + */ +export default async ( + req: Request, + matchedRoute: MatchedRoute +): Promise => { + const uuid = matchedRoute.params.uuid; + const pageNumber = Number(matchedRoute.query.page) || 1; + + const statuses = await Status.find({ + where: { + account: { + id: uuid, + }, + visibility: In(["public", "unlisted"]), + }, + relations: userRelations, + take: 20, + skip: 20 * (pageNumber - 1), + }); + + const totalStatuses = await Status.count({ + where: { + account: { + id: uuid, + }, + visibility: In(["public", "unlisted"]), + }, + relations: userRelations, + }); + + return jsonResponse({ + first: `${getHost()}/users/${uuid}/outbox?page=1`, + last: `${getHost()}/users/${uuid}/outbox?page=1`, + total_items: totalStatuses, + next: + statuses.length === 20 + ? `${getHost()}/users/${uuid}/outbox?page=${pageNumber + 1}` + : undefined, + prev: + pageNumber > 1 + ? `${getHost()}/users/${uuid}/outbox?page=${pageNumber - 1}` + : undefined, + items: statuses.map(s => s.toLysand()), + }); +}; diff --git a/types/lysand/Extension.ts b/types/lysand/Extension.ts new file mode 100644 index 00000000..cf3645a0 --- /dev/null +++ b/types/lysand/Extension.ts @@ -0,0 +1,6 @@ +import { LysandObjectType } from "./Object"; + +export interface ExtensionType extends LysandObjectType { + type: "Extension"; + extension_type: string; +} diff --git a/types/lysand/Object.ts b/types/lysand/Object.ts new file mode 100644 index 00000000..ee7fe964 --- /dev/null +++ b/types/lysand/Object.ts @@ -0,0 +1,164 @@ +import { Emoji } from "./extensions/org.lysand/custom_emojis"; + +export interface LysandObjectType { + type: string; + id: string; // Either a UUID or some kind of time-based UUID-compatible system + uri: string; // URI to the note + created_at: string; + extensions?: { + // Should be in the format + // "organization:extension_name": value + // Example: "org.joinmastodon:spoiler_text": "This is a spoiler!" + "org.lysand:custom_emojis"?: { + emojis: Emoji[]; + }; + "org.lysand:reactions"?: { + reactions: string; + }; + "org.lysand:polls"?: { + poll: { + options: ContentFormat[][]; + votes: number[]; + expires_at: string; + multiple_choice: boolean; + }; + }; + + [key: string]: any; + }; +} + +export interface ActorPublicKeyData { + public_key: string; + actor: string; +} + +export interface Collection { + first: string; + last: string; + next?: string; + prev?: string; + items: T[]; +} + +export interface User extends LysandObjectType { + type: "User"; + bio: ContentFormat[]; + + inbox: string; + outbox: string; + followers: string; + following: string; + liked: string; + disliked: string; + featured: string; + + indexable: boolean; + fields?: { + key: ContentFormat[]; + value: ContentFormat[]; + }[]; + display_name?: string; + public_key?: ActorPublicKeyData; + username: string; + avatar?: ContentFormat[]; + header?: ContentFormat[]; +} + +export interface LysandPublication extends LysandObjectType { + type: "Note" | "Patch"; + author: string; + contents: ContentFormat[]; + mentions: string[]; + replies_to: string[]; + quotes: string[]; + is_sensitive: boolean; + subject: string; + attachments: ContentFormat[][]; +} + +export interface LysandAction extends LysandObjectType { + type: + | "Like" + | "Dislike" + | "Follow" + | "FollowAccept" + | "FollowReject" + | "Announce" + | "Undo"; + author: string; +} + +/** + * A Note is a publication on the network, such as a post or comment + */ +export interface Note extends LysandPublication { + type: "Note"; +} + +/** + * A Patch is an edit to a Note + */ +export interface Patch extends LysandPublication { + type: "Patch"; + patched_id: string; + patched_at: string; +} + +export interface Like extends LysandAction { + type: "Like"; + object: string; +} + +export interface Dislike extends LysandAction { + type: "Dislike"; + object: string; +} + +export interface Announce extends LysandAction { + type: "Announce"; + object: string; +} + +export interface Undo extends LysandAction { + type: "Undo"; + object: string; +} + +export interface Follow extends LysandAction { + type: "Follow"; + followee: string; +} + +export interface FollowAccept extends LysandAction { + type: "FollowAccept"; + follower: string; +} + +export interface FollowReject extends LysandAction { + type: "FollowReject"; + follower: string; +} + +export interface ServerMetadata extends LysandObjectType { + type: "ServerMetadata"; + name: string; + version?: string; + description?: string; + website?: string; + moderators?: string[]; + admins?: string[]; + logo?: ContentFormat[]; + banner?: ContentFormat[]; + supported_extensions?: string[]; +} + +/** + * Content format is an array of objects that contain the content and the content type. + */ +export interface ContentFormat { + content: string; + content_type: string; + description?: string; + size?: string; +} diff --git a/types/lysand/extensions/org.lysand/custom_emojis.ts b/types/lysand/extensions/org.lysand/custom_emojis.ts new file mode 100644 index 00000000..f47f770f --- /dev/null +++ b/types/lysand/extensions/org.lysand/custom_emojis.ts @@ -0,0 +1,7 @@ +import { ContentFormat } from "../../Object"; + +export interface Emoji { + name: string; + url: ContentFormat[]; + alt?: string; +} diff --git a/types/lysand/extensions/org.lysand/polls.ts b/types/lysand/extensions/org.lysand/polls.ts new file mode 100644 index 00000000..af157364 --- /dev/null +++ b/types/lysand/extensions/org.lysand/polls.ts @@ -0,0 +1,14 @@ +import { ExtensionType } from "../../Extension"; + +export interface OrgLysandPollsVoteType extends ExtensionType { + extension_type: "org.lysand:polls/Vote"; + author: string; + poll: string; + option: number; +} + +export interface OrgLysandPollsVoteResultType extends ExtensionType { + extension_type: "org.lysand:polls/VoteResult"; + poll: string; + votes: number[]; +} diff --git a/types/lysand/extensions/org.lysand/reactions.ts b/types/lysand/extensions/org.lysand/reactions.ts new file mode 100644 index 00000000..b18d5256 --- /dev/null +++ b/types/lysand/extensions/org.lysand/reactions.ts @@ -0,0 +1,8 @@ +import { ExtensionType } from "../../Extension"; + +export interface OrgLysandReactionsType extends ExtensionType { + extension_type: "org.lysand:reactions/Reaction"; + author: string; + object: string; + content: string; +} \ No newline at end of file diff --git a/utils/config.ts b/utils/config.ts index 7f851f5f..fb3d7179 100644 --- a/utils/config.ts +++ b/utils/config.ts @@ -16,6 +16,13 @@ export interface ConfigType { banned_ips: string[]; }; + instance: { + name: string; + description: string; + banner: string; + logo: string; + }; + smtp: { server: string; port: number; @@ -133,6 +140,12 @@ export const configDefaults: ConfigType = { password: "postgres", database: "lysand", }, + instance: { + banner: "", + description: "", + logo: "", + name: "", + }, smtp: { password: "", port: 465,