From 325a8ad9adaedc229a9188d0d679693934f8d500 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 20 Oct 2024 04:39:40 +0200 Subject: [PATCH 01/22] Rename `graphql_test` to `graphql_stratz_test` --- tests/{graphql_test.ts => graphql_stratz_test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{graphql_test.ts => graphql_stratz_test.ts} (100%) diff --git a/tests/graphql_test.ts b/tests/graphql_stratz_test.ts similarity index 100% rename from tests/graphql_test.ts rename to tests/graphql_stratz_test.ts From 7ce86f3036b55fa5d84e00cd58e5ce0e5974a059 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 20 Oct 2024 06:13:09 +0200 Subject: [PATCH 02/22] Refactor all existing code --- src/deno/logging.ts | 18 ++++ src/deno/router.ts | 41 +++++++ src/deno/server.ts | 31 ++++++ src/dv/dotinoVeloce.ts | 200 +++++++++++++++++++++++++++++++++++ src/dv/hostMeta.ts | 20 ++++ src/federation.ts | 140 ------------------------ src/fedify/kv.ts | 12 +++ src/fedify/redis.ts | 11 ++ src/graphql/stratz.ts | 99 ----------------- src/handler.ts | 40 ------- src/main.ts | 41 ++++--- src/redis.ts | 11 -- src/stratz/api.ts | 143 +++++++++++++++++++++++++ tests/graphql_stratz_test.ts | 2 +- 14 files changed, 497 insertions(+), 312 deletions(-) create mode 100644 src/deno/logging.ts create mode 100644 src/deno/router.ts create mode 100644 src/deno/server.ts create mode 100644 src/dv/dotinoVeloce.ts create mode 100644 src/dv/hostMeta.ts delete mode 100644 src/federation.ts create mode 100644 src/fedify/kv.ts create mode 100644 src/fedify/redis.ts delete mode 100644 src/graphql/stratz.ts delete mode 100644 src/handler.ts delete mode 100644 src/redis.ts create mode 100644 src/stratz/api.ts diff --git a/src/deno/logging.ts b/src/deno/logging.ts new file mode 100644 index 0000000..866f0b7 --- /dev/null +++ b/src/deno/logging.ts @@ -0,0 +1,18 @@ +import { configure, getConsoleSink, getLogger } from "@logtape/logtape" + + +const l = getLogger(["dotino-veloce", "deno", "logging"]) + + +export async function initLogging() { + await configure({ + sinks: { console: getConsoleSink() }, + filters: {}, + loggers: [ + { category: ["logtape", "meta"], sinks: ["console"], level: "warning" }, + { category: ["fedify"], sinks: ["console"], level: "info" }, + { category: ["dotino-veloce"], sinks: ["console"], level: "debug" }, + ], + }) + l.info`Logging initialized successfully!` +} diff --git a/src/deno/router.ts b/src/deno/router.ts new file mode 100644 index 0000000..9ec7246 --- /dev/null +++ b/src/deno/router.ts @@ -0,0 +1,41 @@ +import { getLogger } from "@logtape/logtape" +import { handleHostMeta } from "../dv/hostMeta.ts" +import { DotinoVeloce } from "../dv/dotinoVeloce.ts" + + +const l = getLogger(["dotino-veloce", "deno", "router"]) + +export function createRouter(ap: DotinoVeloce) { + return async function router(request: Request, _info: Deno.ServeHandlerInfo): Promise { + l.debug`Routing request: ${request}` + + l.debug`Determining request's User-Agent...` + const agent = request.headers.get("User-Agent") + l.debug`Request's User-Agent is: ${agent}` + + l.debug`Determining request's URL...` + const url = new URL(request.url) + l.debug`Request's URL is: ${url}` + + l.debug`Determining request's pathname...` + const pathname = url.pathname + l.debug`Request's pathname is: ${pathname}` + + if (url.pathname === "/.well-known/host-meta") + { + l.debug`Delegating handling to host-meta generator...` + return handleHostMeta(url.origin) + } + else + { + l.debug`Delegating request to Fedify...` + return ap.federation.fetch( + request, + { + contextData: undefined, + }, + ) + } + } +} + diff --git a/src/deno/server.ts b/src/deno/server.ts new file mode 100644 index 0000000..f868cc9 --- /dev/null +++ b/src/deno/server.ts @@ -0,0 +1,31 @@ +import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" + + +const l = getLogger(["dotino-veloce", "deno", "server"]) + + +export function doServe(router: Deno.ServeHandler) { + l.info`Starting server...` + Deno.serve( + { + port: 8080, + + onListen(localAddr) { + l.info`Listening on: ${localAddr}` + }, + + // deno-lint-ignore require-await + async onError(error) { + l.error`Error caught at the serve boundary: ${error}` + return new Response( + "An internal server error has occoured.", + { + status: 500, + } + ) + } + }, + + router, + ) +} \ No newline at end of file diff --git a/src/dv/dotinoVeloce.ts b/src/dv/dotinoVeloce.ts new file mode 100644 index 0000000..a99140d --- /dev/null +++ b/src/dv/dotinoVeloce.ts @@ -0,0 +1,200 @@ +// deno-lint-ignore-file require-await +import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor } from "@fedify/fedify" +import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" +import { escapeHtml } from "@@x/escape" +import { StratzAPI } from "../stratz/api.ts" + + +const l = getLogger(["dotino-veloce", "ap", "federation"]) + + +type ContextData = undefined + + +export class DotinoVeloce { + stratz: StratzAPI + federation: Federation + + constructor(kv: KvStore, stratz: StratzAPI) { + this.stratz = stratz + this.federation = createFederation({ kv }) + this.federation.setInboxListeners("/inbox/{identifier}", /* We don't need shared inboxes here. */) + this.federation.setActorDispatcher("/users/{identifier}", this.#actorDispatcher) + } + + #commonActorProperties(ctx: Context, handle: string): Partial { + l.debug`Generating common actor properties for ${handle}...` + const properties = { + id: ctx.getActorUri(handle), + inbox: ctx.getInboxUri(handle), + preferredUsername: handle, + // Akkoma expects URL to be equal to ID + // Or does it? This makes no sense to me... + // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/object/fetcher.ex#L287 + // url: id, + // Akkoma requires inboxes to be setup to display profiles + // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/activity_pub/object_validators/user_validator.ex#L72 + } + l.debug`Generated common actor properties for ${handle}: ${properties}` + return properties + } + + static playerMatcher = /u([0-9]+)/ + + async #playerActor(ctx: Context, handle: string): Promise { + l.debug`Checking if handle ${handle} matches regex for player accounts...` + const match = handle.match(DotinoVeloce.playerMatcher) + if(!match) { + l.debug`No match for player account regex, returning null.` + return null + } + + l.debug`Getting handle ${handle}'s SteamID...` + const steamId = Number.parseInt(match[1] as any) + l.debug`Handle ${handle}'s SteamID seems to be: ${steamId}` + + l.debug`Making sure the SteamID parsing didn't explode...` + if(Number.isFinite(steamId)) { + l.error`SteamID parsing for ${handle} exploded with ${steamId}, returning null.` + return null + } + + l.debug`Getting player data for ${steamId}...` + const player = await this.stratz.doQueryPlayer(steamId) + if(player === null) { + l.debug`No Steam account was found with ID: ${steamId}` + return null + } + + l.debug`Generating ActivityPub actor for player ${steamId}...` + const actor = new Person({ + ...this.#commonActorProperties(ctx, handle), + + name: `[TEST] ${escapeHtml(player.name)}`, + icon: new Image({ + url: new URL(player.avatar), + mediaType: "image/jpeg" + }), + attachments: [ + new PropertyValue({ + name: "Steam", + value: `https://steamcommunity.com/profiles/[U:1:${player.id}]`, + }), + new PropertyValue({ + name: "STRATZ", + value: `https://stratz.com/players/${player.id}`, + }), + new PropertyValue({ + name: "OpenDota", + value: `https://www.opendota.com/players/${player.id}`, + }), + new PropertyValue({ + name: "DOTABUFF", + value: `https://www.dotabuff.com/players/${player.id}`, + }), + ] + }) + l.debug`Generated ActivityPub actor for player ${steamId}: ${actor}` + + return actor + } + + static guildMatcher = /g([0-9]+)/ + + async #guildActor(ctx: Context, handle: string): Promise { + l.debug`Checking if handle ${handle} matches regex for guild accounts...` + const match = handle.match(DotinoVeloce.guildMatcher) + if(!match) { + l.debug`No match for guild account regex, returning null.` + return null + } + + l.debug`Getting handle ${handle}'s Guild ID...` + const guildId = Number.parseInt(match[1] as any) + l.debug`Handle ${handle}'s Guild ID seems to be: ${guildId}` + + l.debug`Making sure the Guild ID parsing didn't explode...` + if(Number.isFinite(guildId)) { + l.error`Guild ID parsing for ${handle} exploded with ${guildId}, returning null.` + return null + } + + + const guild = await this.stratz.doQueryGuild(guildId) + + if(guild === null) { + l.warn`No guild was found with ID: ${guildId}` + return null + } + + l.debug`Generating ActivityPub actor for guild ${guildId}...` + const actor = new Organization({ + ...this.#commonActorProperties(ctx, handle), + + name: `[TEST] ${escapeHtml(guild.name)}`, + summary: escapeHtml(guild.description), + icon: new Image({ + url: new URL(`https://steamusercontent-a.akamaihd.net/ugc/${guild.logo}`), + mediaType: "image/jpeg" + }), + published: Temporal.Instant.fromEpochMilliseconds(guild.createdDateTime * 1000), + attachments: [ + new PropertyValue({ + name: "Tag", + value: `[${escapeHtml(guild.tag)}]`, + }), + new PropertyValue({ + name: "Message of the day", + value: escapeHtml(guild.motd), + }), + new PropertyValue({ + name: "STRATZ", + value: `https://stratz.com/guilds/${guild.id}`, + }), + ] + }) + l.debug`Generated ActivityPub actor for guild ${guildId}: ${actor}` + + return actor + } + + static serviceMatcher = /service/ + + async #serviceActor(ctx: Context, handle: string): Promise { + l.debug`Checking if handle ${handle} matches regex for the service account...` + const match = handle.match(DotinoVeloce.serviceMatcher) + if(!match) { + l.debug`No match for service account regex, returning null.` + return null + } + + l.debug`Generating ActivityPub actor for service...` + const actor = new Application({ + ...this.#commonActorProperties(ctx, handle), + + name: "[TEST] Dotino Veloce", + summary: "Service account of a Dotino Veloce instance.", + attachments: [ + new PropertyValue({ + name: "Source code", + value: `https://forge.steffo.eu/steffo/dotino-veloce`, + }), + ] + }) + + l.debug`Generated ActivityPub actor for service: ${actor}` + return actor + } + + async #actorDispatcher(ctx: any, handle: string) { + l.debug`Handling actor with handle: ${handle}` + + let actor = null + + actor ??= this.#playerActor(ctx, handle) + actor ??= this.#guildActor(ctx, handle) + actor ??= this.#serviceActor(ctx, handle) + + return actor + } +} diff --git a/src/dv/hostMeta.ts b/src/dv/hostMeta.ts new file mode 100644 index 0000000..e5986ea --- /dev/null +++ b/src/dv/hostMeta.ts @@ -0,0 +1,20 @@ +import { getLogger } from "@logtape/logtape" + + +const l = getLogger(["dotino-veloce", "ap", "hostMeta"]) + + +export function handleHostMeta(origin: string): Response { + l.info`Creating host-meta definition for: ${origin}` + + // Akkoma expects host-meta to be correctly setup + // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/web_finger.ex#L177 + return new Response( + ``, + { + headers: { + "Content-Type": "application/xml", + }, + }, + ) +} diff --git a/src/federation.ts b/src/federation.ts deleted file mode 100644 index 563dad2..0000000 --- a/src/federation.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { createFederation, Person, Application, Image, PropertyValue, Organization, Group } from "@fedify/fedify" -import { kv } from "./redis.ts" -import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" -import { doQueryGuild, doQueryPlayer } from "./graphql/stratz.ts" -import { escapeHtml } from "@@x/escape" - -const l = getLogger(["dotino-veloce", "federation"]) - -l.debug`Creating federation object...` -export const federation = createFederation({ kv }) - -const userMatcher = /u([0-9]+)/ -const guildMatcher = /g([0-9]+)/ - -l.debug`Creating actor dispatcher...` -// deno-lint-ignore require-await -async function actorDispatcher(ctx: any, handle: string) { - l.debug`Determining id of requested actor: ${handle}` - const id = ctx.getActorUri(handle) - l.debug`Requested actor is: ${id.href}` - - if (handle === "service") { - l.info`Dispatching service account...` - return new Application({ - id, - preferredUsername: id.href, - // Akkoma expects URL to be equal to ID - // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/object/fetcher.ex#L287 - url: id, - // Akkoma requires inboxes to be setup to display profiles - // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/activity_pub/object_validators/user_validator.ex#L72 - inbox: ctx.getInboxUri(handle), - - name: "[TEST] Dotino Veloce", - summary: "Service account of a Dotino Veloce instance.", - }) - } - - const steamId = Number.parseInt(handle.match(userMatcher)?.[1] as any) - if(Number.isFinite(steamId)) { - l.info`Dispatching user account with Steam ID: ${steamId}` - - const player = await doQueryPlayer(steamId) - - if(player === null) { - l.warn`No Steam account was found with ID: ${steamId}` - return null - } - - return new Person({ - id, - preferredUsername: id.href, - // Akkoma expects URL to be equal to ID - // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/object/fetcher.ex#L287 - url: id, - // Akkoma requires inboxes to be setup to display profiles - // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/activity_pub/object_validators/user_validator.ex#L72 - inbox: ctx.getInboxUri(handle), - - name: `[TEST] ${escapeHtml(player.name)}`, - icon: new Image({ - url: new URL(player.avatar), - mediaType: "image/jpeg" - }), - attachments: [ - new PropertyValue({ - name: "Steam", - value: `https://steamcommunity.com/profiles/[U:1:${player.id}]`, - }), - new PropertyValue({ - name: "STRATZ", - value: `https://stratz.com/players/${player.id}`, - }), - new PropertyValue({ - name: "OpenDota", - value: `https://www.opendota.com/players/${player.id}`, - }), - new PropertyValue({ - name: "DOTABUFF", - value: `https://www.dotabuff.com/players/${player.id}`, - }), - ] - }) - } - - const guildId = Number.parseInt(handle.match(guildMatcher)?.[1] as any) - if(Number.isFinite(guildId)) { - l.info`Dispatching guild with Guild ID: ${guildId}` - - const guild = await doQueryGuild(guildId) - - if(guild === null) { - l.warn`No guild was found with ID: ${guildId}` - return null - } - - return new Group({ - id, - preferredUsername: id.href, - // Akkoma expects URL to be equal to ID - // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/object/fetcher.ex#L287 - url: id, - // Akkoma requires inboxes to be setup to display profiles - // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/activity_pub/object_validators/user_validator.ex#L72 - inbox: ctx.getInboxUri(handle), - - name: `[TEST] ${escapeHtml(guild.name)}`, - summary: escapeHtml(guild.description), - icon: new Image({ - url: new URL(`https://steamusercontent-a.akamaihd.net/ugc/${guild.logo}`), - mediaType: "image/jpeg" - }), - published: Temporal.Instant.fromEpochMilliseconds(guild.createdDateTime * 1000), - attachments: [ - new PropertyValue({ - name: "Tag", - value: `[${escapeHtml(guild.tag)}]`, - }), - new PropertyValue({ - name: "Message of the day", - value: escapeHtml(guild.motd), - }), - new PropertyValue({ - name: "STRATZ", - value: `https://stratz.com/guilds/${guild.id}`, - }), - ] - }) - } - - l.warn`No dispatcher was found for handle: ${handle}` - return null - -} - -l.debug`Connecting actor dispatcher to federation object...` -federation.setActorDispatcher("/users/{identifier}", actorDispatcher) - -l.debug`Initializing inbox listener...` -federation.setInboxListeners("/inbox/{identifier}") // I don't really care about the shared inbox for this project diff --git a/src/fedify/kv.ts b/src/fedify/kv.ts new file mode 100644 index 0000000..8eb7050 --- /dev/null +++ b/src/fedify/kv.ts @@ -0,0 +1,12 @@ +import { Redis } from "@@npm/ioredis" +import { RedisKvStore } from "@fedify/redis/kv" +import { getLogger } from "@logtape/logtape" + + +const l = getLogger(["dotino-veloce", "fedify", "kv"]) + + +export function createRedisKvStore(redis: Redis) { + l.debug`Creating Redis key-value store...` + return new RedisKvStore(redis, {}) +} diff --git a/src/fedify/redis.ts b/src/fedify/redis.ts new file mode 100644 index 0000000..1838846 --- /dev/null +++ b/src/fedify/redis.ts @@ -0,0 +1,11 @@ +import { getLogger } from "@logtape/logtape" +import { Redis } from "@@npm/ioredis" + + +const l = getLogger(["dotino-veloce", "fedify", "redis"]) + + +export function createRedis() { + l.debug`Creating Redis object...` + return new Redis({}) +} diff --git a/src/graphql/stratz.ts b/src/graphql/stratz.ts deleted file mode 100644 index 6bf4e65..0000000 --- a/src/graphql/stratz.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { getLogger } from "@logtape/logtape" - -const l = getLogger(["dotino-veloce", "graphql"]) - -l.debug`Getting Stratz API URL from environment variable DOTINO_STRATZ_URL...` -const urlString = Deno.env.get("DOTINO_STRATZ_URL") -if(!urlString) { - l.error`DOTINO_STRATZ_URL is unset.` - throw new Error("DOTINO_STRATZ_URL is unset.") -} - -l.debug`Attempting to parse Stratz API URL...` -const url = new URL(urlString) - -l.debug`Getting Stratz API key from environment variable DOTINO_STRATZ_KEY...` -const key = Deno.env.get("DOTINO_STRATZ_KEY") -if(!key) { - l.error`DOTINO_STRATZ_KEY is unset.` - throw new Error("DOTINO_STRATZ_KEY is unset.") -} - -// Little trick to get VSCode to highlight queries! -const graphql = String.raw - -export async function doQuery(query: string, variables: object) { - const request = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": `Bearer ${key}`, - "User-Agent": "STRATZ_API", - }, - body: JSON.stringify({query, variables}) - }) - const data = await request.json() - - if(data["error"]) { - throw new Error("GraphQL query failed.", data["error"]) - } - - return data["data"] -} - -export async function doQueryPlayer(steamId: number): Promise<{ - communityVisibleState: number, - isAnonymous: boolean, - id: number, - name: string, - avatar: string, -} | null> { - l.info`Querying player ${steamId} on the Stratz API...` - const response = await doQuery( - graphql` - query ($steamId: Long!) { - player (steamAccountId: $steamId) { - steamAccount { - communityVisibleState - isAnonymous - id - name - avatar - } - } - } - `, - {steamId} - ) - return response?.player?.steamAccount ?? null -} - -export async function doQueryGuild(guildId: number): Promise<{ - id: number, - motd: string, - name: string, - tag: string, - logo: string, - description: string, - createdDateTime: number, -} | null> { - l.info`Querying guild ${guildId} on the Stratz API...` - const response = await doQuery( - graphql` - query ($guildId: Int!) { - guild(id: $guildId) { - id, - motd, - name, - tag, - logo, - description, - createdDateTime, - } - } - `, - {guildId} - ) - return response?.guild ?? null -} diff --git a/src/handler.ts b/src/handler.ts deleted file mode 100644 index da7fb02..0000000 --- a/src/handler.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { getLogger } from "@logtape/logtape" -import { behindProxy, Fetch } from "@hongminhee/x-forwarded-fetch" -import { federation } from "./federation.ts" - -const l = getLogger(["dotino-veloce", "handler"]) - -l.debug`Creating Deno handler...` -function handler(request: Request, _info: Deno.ServeHandlerInfo) { - l.debug`Received a request, processing...` - - const agent = request.headers.get("User-Agent") - const requestUrl = new URL(request.url) - l.debug`Received request from ${agent} to ${requestUrl.href}` - - // Akkoma expects host-meta to be correctly setup - // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/web_finger.ex#L177 - l.debug`Routing request to: ${requestUrl.pathname}` - if (requestUrl.pathname === "/.well-known/host-meta") { - l.debug`Intercepting request to inject host-meta for ${requestUrl.origin}` - return new Response( - ``, - { - headers: { - "Content-Type": "application/xml", - }, - }, - ) - } - - l.debug`Delegating request to Federation...` - return federation.fetch( - request, - { - contextData: undefined, - }, - ) -} - -l.debug`Creating proxyied Deno handler...` -export const proxyHandler: Deno.ServeHandler = behindProxy(handler as Fetch) // Should be good. diff --git a/src/main.ts b/src/main.ts index 82c2f3a..4afbac7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,24 +1,23 @@ -import { configure, getConsoleSink, getLogger } from "@logtape/logtape" -import { proxyHandler } from "./handler.ts" +import { doServe } from "./deno/server.ts" +import { createRedis } from "./fedify/redis.ts" +import { initLogging } from "./deno/logging.ts" +import { createRedisKvStore } from "./fedify/kv.ts" +import { StratzAPI } from "./stratz/api.ts" +import { behindProxy, Fetch } from "@hongminhee/x-forwarded-fetch" +import { createRouter } from "./deno/router.ts" +import { DotinoVeloce } from "./dv/dotinoVeloce.ts" -await configure({ - sinks: { console: getConsoleSink() }, - filters: {}, - loggers: [ - { category: ["logtape", "meta"], sinks: ["console"], level: "warning" }, - { category: ["fedify"], sinks: ["console"], level: "info" }, - { category: ["dotino-veloce"], sinks: ["console"], level: "debug" }, - ], -}) -const l = getLogger(["dotino-veloce", "main"]) +async function main() { + await initLogging() + const redis = createRedis() + const kv = createRedisKvStore(redis) + const stratz = StratzAPI.fromEnv() + const ap = new DotinoVeloce(kv, stratz) + const router = createRouter(ap) + const routerBehindProxy = behindProxy(router as Fetch) + await doServe(routerBehindProxy) +} -l.info`Starting server...` -Deno.serve( - { - port: 8080, - onListen: (_localAddr) => { - }, - }, - proxyHandler, -) + +main() diff --git a/src/redis.ts b/src/redis.ts deleted file mode 100644 index deae4f9..0000000 --- a/src/redis.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { RedisKvStore } from "@fedify/redis" -import { getLogger } from "@logtape/logtape" -import { Redis } from "@@npm/ioredis" - -const l = getLogger(["dotino-veloce", "redis"]) - -l.debug`Creating redis object...` -export const redis = new Redis({}) - -l.debug`Creating federation object...` -export const kv = new RedisKvStore(redis, {}) diff --git a/src/stratz/api.ts b/src/stratz/api.ts new file mode 100644 index 0000000..fe35a60 --- /dev/null +++ b/src/stratz/api.ts @@ -0,0 +1,143 @@ +import { getLogger } from "@logtape/logtape" + + +const l = getLogger(["dotino-veloce", "stratz", "api"]) +const graphql = String.raw + + +export type QueryPlayer = { + communityVisibleState: number, + isAnonymous: boolean, + id: number, + name: string, + avatar: string, +} + +export type QueryGuild = { + id: number, + motd: string, + name: string, + tag: string, + logo: string, + description: string, + createdDateTime: number, +} + +export class StratzAPI { + url: URL + key: string + + constructor(url: string, key: string) { + l.info`Creating new Stratz API client sending requests at: ${url}` + + l.debug`Using Stratz API key: ${key}` + this.key = key + + l.debug`Attempting to parse Stratz API URL...` + this.url = new URL(url) + l.debug`Stratz API URL is: ${this.url}` + } + + static fromEnv(): StratzAPI { + l.debug`Getting Stratz API URL from environment variable DOTINO_STRATZ_URL...` + const url = Deno.env.get("DOTINO_STRATZ_URL") + if(!url) { + l.error`DOTINO_STRATZ_URL is unset.` + throw new Error("DOTINO_STRATZ_URL is unset.") + } + + l.debug`Getting Stratz API key from environment variable DOTINO_STRATZ_KEY...` + const key = Deno.env.get("DOTINO_STRATZ_KEY") + if(!key) { + l.error`DOTINO_STRATZ_KEY is unset.` + throw new Error("DOTINO_STRATZ_KEY is unset.") + } + + return new StratzAPI(url, key) + } + + async doQuery(query: string, variables: object): Promise { + l.debug`Sending to Stratz API at ${this.url} query ${query} with variables ${variables}` + const response = await fetch(this.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": `Bearer ${this.key}`, + "User-Agent": "STRATZ_API", // Stratz requires this specific User-Agent in API calls + }, + body: JSON.stringify({query, variables}) + }) + l.debug`Received Stratz API response: ${response}` + + l.debug`Parsing Stratz API response as JSON...` + const parsed = await response.json() + l.debug`Parsed Stratz API response as: ${parsed}` + + const error = parsed["error"] + if(error) { + l.error`GraphQL query failed: ${error}` + throw new Error("GraphQL query failed.", error) + } + + l.debug`Attempting to get data from the parsed API response...` + const data = parsed["data"] + l.debug`Got data from the parsed API response: ${data}` + + return data + } + + async doQueryPlayer(steamId: number): Promise { + const query = graphql` + query ($steamId: Long!) { + player (steamAccountId: $steamId) { + steamAccount { + communityVisibleState + isAnonymous + id + name + avatar + } + } + } + ` + const variables = {steamId} + + l.info`Querying player ${steamId} on the Stratz API...` + const response = await this.doQuery(query, variables) + l.debug`Player query successful, received: ${response}` + + l.debug`Attempting to retrieve player's Steam account from the Stratz API response...` + const steamAccount = response?.player?.steamAccount ?? null + l.debug`Player's Steam account is: ${steamAccount}` + + return steamAccount + } + + async doQueryGuild(guildId: number): Promise { + const query = graphql` + query ($guildId: Int!) { + guild(id: $guildId) { + id, + motd, + name, + tag, + logo, + description, + createdDateTime, + } + } + ` + const variables = {guildId} + + l.info`Querying guild ${guildId} on the Stratz API...` + const response = await this.doQuery(query, variables) + l.debug`Player query successful, received: ${response}` + + l.debug`Attempting to retrieve guild data from the Stratz API response...` + const guildData = response?.guild ?? null + l.debug`Guild data is: ${guildData}` + + return guildData + } +} diff --git a/tests/graphql_stratz_test.ts b/tests/graphql_stratz_test.ts index 23ec1cf..252c82f 100644 --- a/tests/graphql_stratz_test.ts +++ b/tests/graphql_stratz_test.ts @@ -1,5 +1,5 @@ import { configure, getConsoleSink } from "@logtape/logtape" -import { doQueryPlayer } from "../src/graphql/stratz.ts" +import { doQueryPlayer } from "../src/stratz/api.ts" import { assert, assertEquals} from "@std/assert" From 42c7f73bf5323abf5e474f576114e4e19ce8a8d9 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 20 Oct 2024 06:22:27 +0200 Subject: [PATCH 03/22] Serve a favicon --- src/deno/router.ts | 9 +++++++-- src/dv/favicon.ts | 18 ++++++++++++++++++ src/dv/hostMeta.ts | 3 ++- 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 src/dv/favicon.ts diff --git a/src/deno/router.ts b/src/deno/router.ts index 9ec7246..ece7f03 100644 --- a/src/deno/router.ts +++ b/src/deno/router.ts @@ -1,6 +1,7 @@ import { getLogger } from "@logtape/logtape" import { handleHostMeta } from "../dv/hostMeta.ts" import { DotinoVeloce } from "../dv/dotinoVeloce.ts" +import { handleFavicon } from "../dv/favicon.ts" const l = getLogger(["dotino-veloce", "deno", "router"]) @@ -21,15 +22,19 @@ export function createRouter(ap: DotinoVeloce) { const pathname = url.pathname l.debug`Request's pathname is: ${pathname}` + if (url.pathname === "/favicon.ico") { + l.debug`Delegating handling to favicon handler...` + return await handleFavicon() + } if (url.pathname === "/.well-known/host-meta") { l.debug`Delegating handling to host-meta generator...` - return handleHostMeta(url.origin) + return await handleHostMeta(url.origin) } else { l.debug`Delegating request to Fedify...` - return ap.federation.fetch( + return await ap.federation.fetch( request, { contextData: undefined, diff --git a/src/dv/favicon.ts b/src/dv/favicon.ts new file mode 100644 index 0000000..04e9045 --- /dev/null +++ b/src/dv/favicon.ts @@ -0,0 +1,18 @@ +import { getLogger } from "@logtape/logtape" + + +const l = getLogger(["dotino-veloce", "ap", "favicon"]) + + +export async function handleFavicon(): Promise { + l.info`Returning favicon...` + + return new Response( + await Deno.readFile(".media/icon-128x128_round.png"), + { + headers: { + "Content-Type": "image/png", + }, + }, + ) +} diff --git a/src/dv/hostMeta.ts b/src/dv/hostMeta.ts index e5986ea..d7f6a1d 100644 --- a/src/dv/hostMeta.ts +++ b/src/dv/hostMeta.ts @@ -4,7 +4,8 @@ import { getLogger } from "@logtape/logtape" const l = getLogger(["dotino-veloce", "ap", "hostMeta"]) -export function handleHostMeta(origin: string): Response { +// deno-lint-ignore require-await +export async function handleHostMeta(origin: string): Promise { l.info`Creating host-meta definition for: ${origin}` // Akkoma expects host-meta to be correctly setup From b22139ea73a4b1556f548ef6bf4baf05d9ca5b9b Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 20 Oct 2024 06:25:20 +0200 Subject: [PATCH 04/22] Rename actor dispatcher to actor handler --- src/dv/dotinoVeloce.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dv/dotinoVeloce.ts b/src/dv/dotinoVeloce.ts index a99140d..470ff87 100644 --- a/src/dv/dotinoVeloce.ts +++ b/src/dv/dotinoVeloce.ts @@ -19,7 +19,7 @@ export class DotinoVeloce { this.stratz = stratz this.federation = createFederation({ kv }) this.federation.setInboxListeners("/inbox/{identifier}", /* We don't need shared inboxes here. */) - this.federation.setActorDispatcher("/users/{identifier}", this.#actorDispatcher) + this.federation.setActorDispatcher("/users/{identifier}", this.#actorHandler) } #commonActorProperties(ctx: Context, handle: string): Partial { @@ -186,7 +186,7 @@ export class DotinoVeloce { return actor } - async #actorDispatcher(ctx: any, handle: string) { + async #actorHandler(ctx: any, handle: string) { l.debug`Handling actor with handle: ${handle}` let actor = null From fca9d4eb7fca80a085a95fa2bf2f9e7b18f7c32f Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 20 Oct 2024 06:25:51 +0200 Subject: [PATCH 05/22] Add typing to `ctx` --- src/dv/dotinoVeloce.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dv/dotinoVeloce.ts b/src/dv/dotinoVeloce.ts index 470ff87..f6437a7 100644 --- a/src/dv/dotinoVeloce.ts +++ b/src/dv/dotinoVeloce.ts @@ -186,7 +186,7 @@ export class DotinoVeloce { return actor } - async #actorHandler(ctx: any, handle: string) { + async #actorHandler(ctx: Context, handle: string) { l.debug`Handling actor with handle: ${handle}` let actor = null From 1c4ca235b35d181b3d50cbf47d78c5e937b1e878 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 20 Oct 2024 06:58:32 +0200 Subject: [PATCH 06/22] =?UTF-8?q?Fedify=20=E2=86=92=20federation=20handler?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/deno/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deno/router.ts b/src/deno/router.ts index ece7f03..e905f8f 100644 --- a/src/deno/router.ts +++ b/src/deno/router.ts @@ -33,7 +33,7 @@ export function createRouter(ap: DotinoVeloce) { } else { - l.debug`Delegating request to Fedify...` + l.debug`Delegating request to federation handlers...` return await ap.federation.fetch( request, { From 292d3d3e7d7c5c93ba596e07c9d139395d0c25d1 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 20 Oct 2024 06:58:51 +0200 Subject: [PATCH 07/22] Setup follow handler and actor mapper --- src/dv/dotinoVeloce.ts | 59 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/src/dv/dotinoVeloce.ts b/src/dv/dotinoVeloce.ts index f6437a7..0f1acd9 100644 --- a/src/dv/dotinoVeloce.ts +++ b/src/dv/dotinoVeloce.ts @@ -1,5 +1,5 @@ // deno-lint-ignore-file require-await -import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor } from "@fedify/fedify" +import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor, Follow } from "@fedify/fedify" import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" import { escapeHtml } from "@@x/escape" import { StratzAPI } from "../stratz/api.ts" @@ -17,9 +17,16 @@ export class DotinoVeloce { constructor(kv: KvStore, stratz: StratzAPI) { this.stratz = stratz + this.federation = createFederation({ kv }) - this.federation.setInboxListeners("/inbox/{identifier}", /* We don't need shared inboxes here. */) - this.federation.setActorDispatcher("/users/{identifier}", this.#actorHandler) + + this.federation + .setActorDispatcher("/users/{identifier}", this.#actorHandler.bind(this)) + .mapHandle(this.#actorMapper.bind(this)) + + this.federation + .setInboxListeners("/inbox/{identifier}", "/inbox") + .on(Follow, this.#followHandler.bind(this)) } #commonActorProperties(ctx: Context, handle: string): Partial { @@ -187,14 +194,56 @@ export class DotinoVeloce { } async #actorHandler(ctx: Context, handle: string) { - l.debug`Handling actor with handle: ${handle}` + l.info`Handling actor with handle: ${handle}` let actor = null + actor ??= this.#serviceActor(ctx, handle) actor ??= this.#playerActor(ctx, handle) actor ??= this.#guildActor(ctx, handle) - actor ??= this.#serviceActor(ctx, handle) return actor } + + async #actorMapper(_ctx: Context, handle: string) { + return handle + } + + async #followHandler(ctx: Context, follow: Follow) { + l.info`Handling follow request: ${follow}` + + if(!follow.id) { + l.warn`Missing follow ID, skipping.` + return + } + if(!follow.actorId) { + l.warn`Missing actor ID, skipping.` + return + } + if(!follow.objectId) { + l.warn`Missing object ID, skipping.` + return + } + + l.debug`Attempting to determine object of the follow request...` // TODO: ??? + const object = ctx.parseUri(follow.objectId) + l.debug`Object is: ${object}` + + if(!object) { + l.warn`Failed to determine object, skipping.` + return + } + if(object.type !== "actor") { + l.warn`Object type is not actor, skipping.` // TODO: Why? + return + } + + l.debug`Attempting to determine actor of the follow request...` + const actor = await follow.getActor(ctx) + l.debug`Actor is: ${actor}` + + l.debug`Attempting to determine target of the follow request...` + const target = await follow.getTarget(ctx) + l.debug`Target is: ${target}` + } } From 2f54d416256426af6de01c9c46036c03844a5e06 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 20 Oct 2024 07:48:27 +0200 Subject: [PATCH 08/22] Incomplete progress, transition toward postgres --- deno.json | 5 +- deno.lock | 127 ++++++++++++++++++++++++++++++++++++--- src/database/kv.ts | 12 ++++ src/database/postgres.ts | 22 +++++++ src/dv/dotinoVeloce.ts | 15 +++-- src/fedify/kv.ts | 12 ---- src/fedify/redis.ts | 11 ---- src/main.ts | 8 +-- 8 files changed, 171 insertions(+), 41 deletions(-) create mode 100644 src/database/kv.ts create mode 100644 src/database/postgres.ts delete mode 100644 src/fedify/kv.ts delete mode 100644 src/fedify/redis.ts diff --git a/deno.json b/deno.json index 5944e6e..6b0df48 100644 --- a/deno.json +++ b/deno.json @@ -1,13 +1,14 @@ { "imports": { "@@npm/ioredis": "npm:ioredis@^5.4.1", + "@@x/escape": "https://deno.land/x/escape@1.3.0/mod.ts", "@fedify/fedify": "jsr:@fedify/fedify@^1.0.2", - "@fedify/redis": "jsr:@fedify/redis@^0.3.0", + "@fedify/postgres": "jsr:@fedify/postgres@^0.1.0", "@hongminhee/x-forwarded-fetch": "jsr:@hongminhee/x-forwarded-fetch@^0.2.0", "@logtape/logtape": "jsr:@logtape/logtape@^0.6.3", "@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0", "@std/assert": "jsr:@std/assert@1", - "@@x/escape": "https://deno.land/x/escape@1.3.0/mod.ts" + "@@npm/postgres": "npm:postgres@^3.4.4" }, "unstable": [ "temporal" diff --git a/deno.lock b/deno.lock index 16ac9f1..901372b 100644 --- a/deno.lock +++ b/deno.lock @@ -4,7 +4,7 @@ "specifiers": { "jsr:@fedify/fedify@^1.0.0": "jsr:@fedify/fedify@1.0.2", "jsr:@fedify/fedify@^1.0.2": "jsr:@fedify/fedify@1.0.2", - "jsr:@fedify/redis@^0.3.0": "jsr:@fedify/redis@0.3.0", + "jsr:@fedify/postgres@^0.1.0": "jsr:@fedify/postgres@0.1.0", "jsr:@hongminhee/x-forwarded-fetch@^0.2.0": "jsr:@hongminhee/x-forwarded-fetch@0.2.0", "jsr:@hugoalh/http-header-link@^1.0.2": "jsr:@hugoalh/http-header-link@1.0.2", "jsr:@hugoalh/is-string-singleline@1.0.2": "jsr:@hugoalh/is-string-singleline@1.0.2", @@ -20,12 +20,14 @@ "npm:@phensley/language-tag@^1.9.0": "npm:@phensley/language-tag@1.9.0", "npm:@types/node": "npm:@types/node@18.16.19", "npm:asn1js@^3.0.5": "npm:asn1js@3.0.5", + "npm:graffle@next": "npm:graffle@8.0.0-next.68_@opentelemetry+api@1.9.0_graphql@16.9.0", "npm:ioredis@^5.4.1": "npm:ioredis@5.4.1", "npm:json-canon@^1.0.1": "npm:json-canon@1.0.1", "npm:jsonld@^8.3.2": "npm:jsonld@8.3.2", "npm:multibase@^4.0.6": "npm:multibase@4.0.6", "npm:multicodec@^3.2.1": "npm:multicodec@3.2.1", "npm:pkijs@^3.2.4": "npm:pkijs@3.2.4", + "npm:postgres@^3.4.4": "npm:postgres@3.4.4", "npm:uri-template-router@^0.0.16": "npm:uri-template-router@0.0.16", "npm:url-template@^3.1.1": "npm:url-template@3.1.1" }, @@ -50,12 +52,11 @@ "npm:url-template@^3.1.1" ] }, - "@fedify/redis@0.3.0": { - "integrity": "48068af7ad24d4f6c6935d6f869659faeb92281a9e796921f9ba7d94e74f8cfc", + "@fedify/postgres@0.1.0": { + "integrity": "350e3e535372d84acebe7392ae98495e388f30d75165d52c6d32a8dc6e940b80", "dependencies": [ "jsr:@fedify/fedify@^1.0.0", - "jsr:@logtape/logtape@^0.6.3", - "npm:ioredis@^5.4.1" + "npm:postgres@^3.4.4" ] }, "@hongminhee/x-forwarded-fetch@0.2.0": { @@ -108,10 +109,38 @@ "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "dependencies": {} }, + "@graphql-typed-document-node/core@3.2.0_graphql@16.9.0": { + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "dependencies": { + "graphql": "graphql@16.9.0" + } + }, "@ioredis/commands@1.2.0": { "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", "dependencies": {} }, + "@molt/command@0.9.0": { + "integrity": "sha512-1JI8dAlpqlZoXyKWVQggX7geFNPxBpocHIXQCsnxDjKy+3WX4SGyZVJXuLlqRRrX7FmQCuuMAfx642ovXmPA9g==", + "dependencies": { + "@molt/types": "@molt/types@0.2.0", + "alge": "alge@0.8.1", + "chalk": "chalk@5.3.0", + "lodash.camelcase": "lodash.camelcase@4.3.0", + "lodash.snakecase": "lodash.snakecase@4.1.1", + "readline-sync": "readline-sync@1.4.10", + "string-length": "string-length@6.0.0", + "strip-ansi": "strip-ansi@7.1.0", + "ts-toolbelt": "ts-toolbelt@9.6.0", + "type-fest": "type-fest@4.26.1", + "zod": "zod@3.23.8" + } + }, + "@molt/types@0.2.0": { + "integrity": "sha512-p6ChnEZDGjg9PYPec9BK6Yp5/DdSrYQvXTBAtgrnqX6N36cZy37ql1c8Tc5LclfIYBNG7EZp8NBcRTYJwyi84g==", + "dependencies": { + "ts-toolbelt": "ts-toolbelt@9.6.0" + } + }, "@multiformats/base-x@4.0.1": { "integrity": "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==", "dependencies": {} @@ -140,6 +169,19 @@ "event-target-shim": "event-target-shim@5.0.1" } }, + "alge@0.8.1": { + "integrity": "sha512-kiV9nTt+XIauAXsowVygDxMZLplZxDWt0W8plE/nB32/V2ziM/P/TxDbSVK7FYIUt2Xo16h3/htDh199LNPCKQ==", + "dependencies": { + "lodash.ismatch": "lodash.ismatch@4.4.0", + "remeda": "remeda@1.61.0", + "ts-toolbelt": "ts-toolbelt@9.6.0", + "zod": "zod@3.23.8" + } + }, + "ansi-regex@6.1.0": { + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dependencies": {} + }, "asn1js@3.0.5": { "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", "dependencies": { @@ -156,6 +198,10 @@ "integrity": "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==", "dependencies": {} }, + "chalk@5.3.0": { + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dependencies": {} + }, "cluster-key-slot@1.1.2": { "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", "dependencies": {} @@ -191,6 +237,20 @@ "fetch-blob": "fetch-blob@3.2.0" } }, + "graffle@8.0.0-next.68_@opentelemetry+api@1.9.0_graphql@16.9.0": { + "integrity": "sha512-oaigY1yEX48cUSiFJKtigjGw4ooA7JtLwafSUQuuAgbl0qtnaSmAGrfSRpb9RhxF0y/HEzJNEMYUaea085cn8w==", + "dependencies": { + "@graphql-typed-document-node/core": "@graphql-typed-document-node/core@3.2.0_graphql@16.9.0", + "@molt/command": "@molt/command@0.9.0", + "@opentelemetry/api": "@opentelemetry/api@1.9.0", + "graphql": "graphql@16.9.0", + "is-plain-obj": "is-plain-obj@4.1.0" + } + }, + "graphql@16.9.0": { + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "dependencies": {} + }, "ioredis@5.4.1": { "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", "dependencies": { @@ -205,6 +265,10 @@ "standard-as-callback": "standard-as-callback@2.1.0" } }, + "is-plain-obj@4.1.0": { + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dependencies": {} + }, "json-canon@1.0.1": { "integrity": "sha512-PQcj4PFOTAQxE8PgoQ4KrM0DcKWZd7S3ELOON8rmysl9I8JuFMgxu1H9v+oZsTPjjkpeS3IHPwLjr7d+gKygnw==", "dependencies": {} @@ -230,6 +294,10 @@ "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", "dependencies": {} }, + "lodash.camelcase@4.3.0": { + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dependencies": {} + }, "lodash.defaults@4.2.0": { "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "dependencies": {} @@ -238,6 +306,14 @@ "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", "dependencies": {} }, + "lodash.ismatch@4.4.0": { + "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", + "dependencies": {} + }, + "lodash.snakecase@4.1.1": { + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dependencies": {} + }, "lru-cache@6.0.0": { "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { @@ -288,6 +364,10 @@ "tslib": "tslib@2.7.0" } }, + "postgres@3.4.4": { + "integrity": "sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==", + "dependencies": {} + }, "pvtsutils@1.3.5": { "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", "dependencies": { @@ -304,6 +384,10 @@ "setimmediate": "setimmediate@1.0.5" } }, + "readline-sync@1.4.10": { + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "dependencies": {} + }, "redis-errors@1.2.0": { "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", "dependencies": {} @@ -314,6 +398,10 @@ "redis-errors": "redis-errors@1.2.0" } }, + "remeda@1.61.0": { + "integrity": "sha512-caKfSz9rDeSKBQQnlJnVW3mbVdFgxgGWQKq1XlFokqjf+hQD5gxutLGTTY2A/x24UxVyJe9gH5fAkFI63ULw4A==", + "dependencies": {} + }, "setimmediate@1.0.5": { "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dependencies": {} @@ -322,10 +410,30 @@ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "dependencies": {} }, + "string-length@6.0.0": { + "integrity": "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==", + "dependencies": { + "strip-ansi": "strip-ansi@7.1.0" + } + }, + "strip-ansi@7.1.0": { + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "ansi-regex@6.1.0" + } + }, + "ts-toolbelt@9.6.0": { + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "dependencies": {} + }, "tslib@2.7.0": { "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dependencies": {} }, + "type-fest@4.26.1": { + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dependencies": {} + }, "uint8arrays@3.1.1": { "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", "dependencies": { @@ -357,6 +465,10 @@ "yallist@4.0.0": { "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dependencies": {} + }, + "zod@3.23.8": { + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dependencies": {} } } }, @@ -508,12 +620,13 @@ "workspace": { "dependencies": [ "jsr:@fedify/fedify@^1.0.2", - "jsr:@fedify/redis@^0.3.0", + "jsr:@fedify/postgres@^0.1.0", "jsr:@hongminhee/x-forwarded-fetch@^0.2.0", "jsr:@logtape/logtape@^0.6.3", "jsr:@std/assert@1", "npm:@opentelemetry/api@^1.9.0", - "npm:ioredis@^5.4.1" + "npm:ioredis@^5.4.1", + "npm:postgres@^3.4.4" ] } } diff --git a/src/database/kv.ts b/src/database/kv.ts new file mode 100644 index 0000000..72a3b7e --- /dev/null +++ b/src/database/kv.ts @@ -0,0 +1,12 @@ +import { PostgresKvStore } from "@fedify/postgres/kv" +import { getLogger } from "@logtape/logtape" +import Postgres from "@@npm/postgres" + + +const l = getLogger(["dotino-veloce", "fedify", "kv"]) + + +export function createPostgresKvStore(postgres: Postgres.Sql): PostgresKvStore { + l.info`Creating Postgres key-value store...` + return new PostgresKvStore(postgres, {}) +} diff --git a/src/database/postgres.ts b/src/database/postgres.ts new file mode 100644 index 0000000..477d110 --- /dev/null +++ b/src/database/postgres.ts @@ -0,0 +1,22 @@ +import Postgres from "@@npm/postgres" +import { getLogger } from "@logtape/logtape" + + +const l = getLogger(["dotino-veloce", "fedify", "postgres"]) + + +export function createPostgres(connString: string): Postgres.Sql { + l.info`Creating Postgres object with string: ${connString}` + return Postgres(connString) +} + +export function createPostgresFromEnv(): Postgres.Sql { + l.debug`Getting connection string from environment variable DOTINO_POSTGRES_STRING...` + const connString = Deno.env.get("DOTINO_POSTGRES_STRING") + if(!connString) { + l.error`DOTINO_POSTGRES_STRING is unset.` + throw new Error("DOTINO_POSTGRES_STRING is unset.") + } + + return createPostgres(connString) +} diff --git a/src/dv/dotinoVeloce.ts b/src/dv/dotinoVeloce.ts index 0f1acd9..2cea1d7 100644 --- a/src/dv/dotinoVeloce.ts +++ b/src/dv/dotinoVeloce.ts @@ -23,6 +23,7 @@ export class DotinoVeloce { this.federation .setActorDispatcher("/users/{identifier}", this.#actorHandler.bind(this)) .mapHandle(this.#actorMapper.bind(this)) + .setKeyPairsDispatcher(this.#actorKeys.bind(this)) this.federation .setInboxListeners("/inbox/{identifier}", "/inbox") @@ -193,22 +194,26 @@ export class DotinoVeloce { return actor } - async #actorHandler(ctx: Context, handle: string) { + async #actorHandler(ctx: Context, handle: string): Promise { l.info`Handling actor with handle: ${handle}` let actor = null - actor ??= this.#serviceActor(ctx, handle) - actor ??= this.#playerActor(ctx, handle) - actor ??= this.#guildActor(ctx, handle) + actor ??= await this.#serviceActor(ctx, handle) + actor ??= await this.#playerActor(ctx, handle) + actor ??= await this.#guildActor(ctx, handle) return actor } - async #actorMapper(_ctx: Context, handle: string) { + async #actorMapper(_ctx: Context, handle: string): Promise { return handle } + async #actorKeys(ctx: Context, handle: string): Promise { + throw "TODO: Not implemented" + } + async #followHandler(ctx: Context, follow: Follow) { l.info`Handling follow request: ${follow}` diff --git a/src/fedify/kv.ts b/src/fedify/kv.ts deleted file mode 100644 index 8eb7050..0000000 --- a/src/fedify/kv.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Redis } from "@@npm/ioredis" -import { RedisKvStore } from "@fedify/redis/kv" -import { getLogger } from "@logtape/logtape" - - -const l = getLogger(["dotino-veloce", "fedify", "kv"]) - - -export function createRedisKvStore(redis: Redis) { - l.debug`Creating Redis key-value store...` - return new RedisKvStore(redis, {}) -} diff --git a/src/fedify/redis.ts b/src/fedify/redis.ts deleted file mode 100644 index 1838846..0000000 --- a/src/fedify/redis.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { getLogger } from "@logtape/logtape" -import { Redis } from "@@npm/ioredis" - - -const l = getLogger(["dotino-veloce", "fedify", "redis"]) - - -export function createRedis() { - l.debug`Creating Redis object...` - return new Redis({}) -} diff --git a/src/main.ts b/src/main.ts index 4afbac7..ca598a0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,17 +1,17 @@ import { doServe } from "./deno/server.ts" -import { createRedis } from "./fedify/redis.ts" import { initLogging } from "./deno/logging.ts" -import { createRedisKvStore } from "./fedify/kv.ts" import { StratzAPI } from "./stratz/api.ts" import { behindProxy, Fetch } from "@hongminhee/x-forwarded-fetch" import { createRouter } from "./deno/router.ts" import { DotinoVeloce } from "./dv/dotinoVeloce.ts" +import { createPostgresKvStore } from "./database/kv.ts" +import { createPostgresFromEnv } from "./database/postgres.ts" async function main() { await initLogging() - const redis = createRedis() - const kv = createRedisKvStore(redis) + const postgres = createPostgresFromEnv() + const kv = createPostgresKvStore(postgres) const stratz = StratzAPI.fromEnv() const ap = new DotinoVeloce(kv, stratz) const router = createRouter(ap) From 4ebacf675357e8f4f8317dc0b0f94df25e581e6c Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 20 Oct 2024 19:23:01 +0200 Subject: [PATCH 09/22] Fix logger name --- src/database/kv.ts | 2 +- src/database/postgres.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/kv.ts b/src/database/kv.ts index 72a3b7e..97d6aff 100644 --- a/src/database/kv.ts +++ b/src/database/kv.ts @@ -3,7 +3,7 @@ import { getLogger } from "@logtape/logtape" import Postgres from "@@npm/postgres" -const l = getLogger(["dotino-veloce", "fedify", "kv"]) +const l = getLogger(["dotino-veloce", "database", "kv"]) export function createPostgresKvStore(postgres: Postgres.Sql): PostgresKvStore { diff --git a/src/database/postgres.ts b/src/database/postgres.ts index 477d110..9380ad9 100644 --- a/src/database/postgres.ts +++ b/src/database/postgres.ts @@ -2,7 +2,7 @@ import Postgres from "@@npm/postgres" import { getLogger } from "@logtape/logtape" -const l = getLogger(["dotino-veloce", "fedify", "postgres"]) +const l = getLogger(["dotino-veloce", "database", "postgres"]) export function createPostgres(connString: string): Postgres.Sql { From c554cb5936594c643ad77f29a8ce5afa5dd95305 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 21 Oct 2024 03:24:07 +0200 Subject: [PATCH 10/22] Create VSCode launch configs --- .vscode/launch.json | 50 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7da08ca --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,50 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "request": "launch", + "name": "Dev Server", + "type": "node", + "program": "${workspaceFolder}/main.ts", + "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/local.env", + "runtimeExecutable": "/usr/bin/deno", + "runtimeArgs": [ + "run", + "--watch", + "--no-prompt", + "--allow-read='.,$HOME/.cache/deno,$HOME/.cache/node_modules'", + "--allow-env=DOTINO_POSTGRES_STRING,DOTINO_STRATZ_URL,DOTINO_STRATZ_KEY", + "--allow-sys=uid,gid", + "--allow-net=0.0.0.0:8080", + "--inspect-wait", + "src/main.ts" + ], + "attachSimplePort": 9229 + }, + { + + "request": "launch", + "name": "Init database", + "type": "node", + "program": "${workspaceFolder}/main.ts", + "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/local.env", + "runtimeExecutable": "/usr/bin/deno", + "runtimeArgs": [ + "run", + "--no-prompt", + "--allow-read='.'", + "--allow-env=DOTINO_POSTGRES_STRING", + "--allow-sys=uid,gid", + "--allow-net=0.0.0.0:8080", + "--inspect-wait", + "src/database/init/index.ts" + ], + "attachSimplePort": 9229 + }, + ] +} \ No newline at end of file From f490404fb635758adad36c50f9ecc9c3c5f4f433 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 21 Oct 2024 03:24:20 +0200 Subject: [PATCH 11/22] Create database initialization scripts --- src/database/init/01-drop-schema.sql | 1 + src/database/init/02-create-schema.sql | 1 + .../init/03-create-migration_applied.sql | 3 ++ src/database/init/04-create-actor.sql | 9 ++++ src/database/init/05-create-actor_service.sql | 5 +++ src/database/init/06-create-actor_player.sql | 5 +++ src/database/init/07-create-actor_guild.sql | 5 +++ src/database/init/index.ts | 41 +++++++++++++++++++ 8 files changed, 70 insertions(+) create mode 100644 src/database/init/01-drop-schema.sql create mode 100644 src/database/init/02-create-schema.sql create mode 100644 src/database/init/03-create-migration_applied.sql create mode 100644 src/database/init/04-create-actor.sql create mode 100644 src/database/init/05-create-actor_service.sql create mode 100644 src/database/init/06-create-actor_player.sql create mode 100644 src/database/init/07-create-actor_guild.sql create mode 100644 src/database/init/index.ts diff --git a/src/database/init/01-drop-schema.sql b/src/database/init/01-drop-schema.sql new file mode 100644 index 0000000..cae5c3d --- /dev/null +++ b/src/database/init/01-drop-schema.sql @@ -0,0 +1 @@ +DROP SCHEMA public CASCADE; diff --git a/src/database/init/02-create-schema.sql b/src/database/init/02-create-schema.sql new file mode 100644 index 0000000..32e23a6 --- /dev/null +++ b/src/database/init/02-create-schema.sql @@ -0,0 +1 @@ +CREATE SCHEMA public; diff --git a/src/database/init/03-create-migration_applied.sql b/src/database/init/03-create-migration_applied.sql new file mode 100644 index 0000000..7aac154 --- /dev/null +++ b/src/database/init/03-create-migration_applied.sql @@ -0,0 +1,3 @@ +CREATE TABLE migration_applied ( + code CHAR(4) PRIMARY KEY +); diff --git a/src/database/init/04-create-actor.sql b/src/database/init/04-create-actor.sql new file mode 100644 index 0000000..cf073bc --- /dev/null +++ b/src/database/init/04-create-actor.sql @@ -0,0 +1,9 @@ +CREATE TABLE actor ( + handle VARCHAR PRIMARY KEY, + + public_rsassa BYTEA NOT NULL, + private_rsassa BYTEA NOT NULL, + + public_jwk BYTEA NOT NULL, + private_jwk BYTEA NOT NULL +); diff --git a/src/database/init/05-create-actor_service.sql b/src/database/init/05-create-actor_service.sql new file mode 100644 index 0000000..e4318d7 --- /dev/null +++ b/src/database/init/05-create-actor_service.sql @@ -0,0 +1,5 @@ +CREATE TABLE actor_service ( + service_id SERIAL UNIQUE NOT NULL +) INHERITS ( + actor +); diff --git a/src/database/init/06-create-actor_player.sql b/src/database/init/06-create-actor_player.sql new file mode 100644 index 0000000..8ff5637 --- /dev/null +++ b/src/database/init/06-create-actor_player.sql @@ -0,0 +1,5 @@ +CREATE TABLE actor_player ( + steam_id BIGINT UNIQUE NOT NULL +) INHERITS ( + actor +); diff --git a/src/database/init/07-create-actor_guild.sql b/src/database/init/07-create-actor_guild.sql new file mode 100644 index 0000000..62910d6 --- /dev/null +++ b/src/database/init/07-create-actor_guild.sql @@ -0,0 +1,5 @@ +CREATE TABLE actor_guild ( + guild_id INT UNIQUE NOT NULL +) INHERITS ( + actor +); diff --git a/src/database/init/index.ts b/src/database/init/index.ts new file mode 100644 index 0000000..a41c07a --- /dev/null +++ b/src/database/init/index.ts @@ -0,0 +1,41 @@ +import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" +import { createPostgresFromEnv } from "../postgres.ts" +import { initLogging } from "../../deno/logging.ts" + + +const l = getLogger(["dotino-veloce", "database", "init"]) + + +async function main() { + await initLogging() + + l.debug`Creating Postgres instance...` + const postgres = createPostgresFromEnv() + + l.info`01/XX Dropping public schema from database...` + await postgres.file("src/database/init/01-drop-schema.sql") + + l.info`02/XX Recreating public schema...` + await postgres.file("src/database/init/02-create-schema.sql") + + l.info`03/XX Creating applied migrations table...` + await postgres.file("src/database/init/03-create-migration_applied.sql") + + l.info`04/XX Creating actor table...` + await postgres.file("src/database/init/04-create-actor.sql") + + l.info`05/XX Creating service table...` + await postgres.file("src/database/init/05-create-actor_service.sql") + + l.info`06/XX Creating player table...` + await postgres.file("src/database/init/06-create-actor_player.sql") + + l.info`07/XX Creating guild table...` + await postgres.file("src/database/init/07-create-actor_guild.sql") + + l.info("Done!") + Deno.exit(0) +} + + +await main() From 4cb2e53acee027406801fbe6f31bf3c2157d596c Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 21 Oct 2024 03:47:04 +0200 Subject: [PATCH 12/22] Various changes to get the server to launch again --- .vscode/launch.json | 4 ++-- src/deno/server.ts | 2 +- src/dv/dotinoVeloce.ts | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 7da08ca..6a8bec7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,7 +19,7 @@ "--allow-read='.,$HOME/.cache/deno,$HOME/.cache/node_modules'", "--allow-env=DOTINO_POSTGRES_STRING,DOTINO_STRATZ_URL,DOTINO_STRATZ_KEY", "--allow-sys=uid,gid", - "--allow-net=0.0.0.0:8080", + "--allow-net", "--inspect-wait", "src/main.ts" ], @@ -40,7 +40,7 @@ "--allow-read='.'", "--allow-env=DOTINO_POSTGRES_STRING", "--allow-sys=uid,gid", - "--allow-net=0.0.0.0:8080", + "--allow-net", "--inspect-wait", "src/database/init/index.ts" ], diff --git a/src/deno/server.ts b/src/deno/server.ts index f868cc9..d0aaae7 100644 --- a/src/deno/server.ts +++ b/src/deno/server.ts @@ -18,7 +18,7 @@ export function doServe(router: Deno.ServeHandler) { async onError(error) { l.error`Error caught at the serve boundary: ${error}` return new Response( - "An internal server error has occoured.", + "An internal server error has occurred.", { status: 500, } diff --git a/src/dv/dotinoVeloce.ts b/src/dv/dotinoVeloce.ts index 2cea1d7..64ad4ed 100644 --- a/src/dv/dotinoVeloce.ts +++ b/src/dv/dotinoVeloce.ts @@ -1,5 +1,5 @@ // deno-lint-ignore-file require-await -import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor, Follow } from "@fedify/fedify" +import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor, Follow, Endpoints } from "@fedify/fedify" import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" import { escapeHtml } from "@@x/escape" import { StratzAPI } from "../stratz/api.ts" @@ -23,7 +23,7 @@ export class DotinoVeloce { this.federation .setActorDispatcher("/users/{identifier}", this.#actorHandler.bind(this)) .mapHandle(this.#actorMapper.bind(this)) - .setKeyPairsDispatcher(this.#actorKeys.bind(this)) + // .setKeyPairsDispatcher(this.#actorKeys.bind(this)) this.federation .setInboxListeners("/inbox/{identifier}", "/inbox") @@ -35,6 +35,9 @@ export class DotinoVeloce { const properties = { id: ctx.getActorUri(handle), inbox: ctx.getInboxUri(handle), + endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), + }), preferredUsername: handle, // Akkoma expects URL to be equal to ID // Or does it? This makes no sense to me... From e2f2b3702b436560a5c9bea7fb02c8150b39e7d5 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 21 Oct 2024 04:00:59 +0200 Subject: [PATCH 13/22] Permissions are too hard, idc --- .vscode/launch.json | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6a8bec7..c0e5980 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,10 +16,7 @@ "run", "--watch", "--no-prompt", - "--allow-read='.,$HOME/.cache/deno,$HOME/.cache/node_modules'", - "--allow-env=DOTINO_POSTGRES_STRING,DOTINO_STRATZ_URL,DOTINO_STRATZ_KEY", - "--allow-sys=uid,gid", - "--allow-net", + "--allow-all", "--inspect-wait", "src/main.ts" ], @@ -37,10 +34,7 @@ "runtimeArgs": [ "run", "--no-prompt", - "--allow-read='.'", - "--allow-env=DOTINO_POSTGRES_STRING", - "--allow-sys=uid,gid", - "--allow-net", + "--allow-all", "--inspect-wait", "src/database/init/index.ts" ], From 92d543dbbd3f439239cdd7b2c13b33dd070fa058 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 21 Oct 2024 06:20:41 +0200 Subject: [PATCH 14/22] Keys seem to be working! This is wild though: https://fosstodon.org/@hongminhee/113343168981354525 --- .vscode/launch.json | 18 +- src/database/index.ts | 176 ++++++++++++++++++ src/database/init/04-create-actor.sql | 8 +- src/database/init/05-create-actor_service.sql | 5 +- src/database/init/08-insert-actor_service.sql | 13 ++ src/database/init/index.ts | 41 ---- src/database/kv.ts | 12 -- src/database/postgres.ts | 22 --- src/database/query/get-actor_guild.sql | 12 ++ src/database/query/get-actor_player.sql | 12 ++ src/database/query/get-actor_service.sql | 8 + src/dv/dotinoVeloce.ts | 65 +++++-- src/entry/dbInit.ts | 12 ++ src/entry/server.ts | 21 +++ src/main.ts | 23 --- 15 files changed, 315 insertions(+), 133 deletions(-) create mode 100644 src/database/index.ts create mode 100644 src/database/init/08-insert-actor_service.sql delete mode 100644 src/database/init/index.ts delete mode 100644 src/database/kv.ts delete mode 100644 src/database/postgres.ts create mode 100644 src/database/query/get-actor_guild.sql create mode 100644 src/database/query/get-actor_player.sql create mode 100644 src/database/query/get-actor_service.sql create mode 100644 src/entry/dbInit.ts create mode 100644 src/entry/server.ts delete mode 100644 src/main.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index c0e5980..0b30c2a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,9 +6,9 @@ "configurations": [ { "request": "launch", - "name": "Dev Server", + "name": "Reinitialize database", "type": "node", - "program": "${workspaceFolder}/main.ts", + "program": "${workspaceFolder}/src/entry/dbInit.ts", "cwd": "${workspaceFolder}", "envFile": "${workspaceFolder}/local.env", "runtimeExecutable": "/usr/bin/deno", @@ -17,28 +17,26 @@ "--watch", "--no-prompt", "--allow-all", - "--inspect-wait", - "src/main.ts" + "--inspect-wait" ], "attachSimplePort": 9229 }, { - "request": "launch", - "name": "Init database", + "name": "Dev Server", "type": "node", - "program": "${workspaceFolder}/main.ts", + "program": "${workspaceFolder}/src/entry/server.ts", "cwd": "${workspaceFolder}", "envFile": "${workspaceFolder}/local.env", "runtimeExecutable": "/usr/bin/deno", "runtimeArgs": [ "run", + "--watch", "--no-prompt", "--allow-all", - "--inspect-wait", - "src/database/init/index.ts" + "--inspect-wait" ], "attachSimplePort": 9229 - }, + } ] } \ No newline at end of file diff --git a/src/database/index.ts b/src/database/index.ts new file mode 100644 index 0000000..4bce443 --- /dev/null +++ b/src/database/index.ts @@ -0,0 +1,176 @@ +import Postgres from "@@npm/postgres" +import { getLogger } from "@logtape/logtape" +import { exportJwk, generateCryptoKeyPair, KvStore } from "@fedify/fedify" +import { PostgresKvStore } from "@fedify/postgres/kv" + + +const l = getLogger(["dotino-veloce", "database", "postgres"]) + + +export type DbActor = { + handle: string, + public_rsa: JsonWebKey, + public_ed: JsonWebKey, + private_rsa: JsonWebKey, + private_ed: JsonWebKey, +} + +export type DbActorService = DbActor + +export type DbActorPlayer = DbActor & { + steam_id: bigint, +} + +export type DbActorGuild = DbActor & { + guild_id: number, +} + + +export class Database { + sql: Postgres.Sql + + constructor(connString: string) { + l.info`Creating Postgres object with string: ${connString}` + this.sql = Postgres(connString) + } + + static fromEnv() { + l.debug`Getting connection string from environment variable DOTINO_POSTGRES_STRING...` + const connString = Deno.env.get("DOTINO_POSTGRES_STRING") + if(!connString) { + l.error`DOTINO_POSTGRES_STRING is unset.` + throw new Error("DOTINO_POSTGRES_STRING is unset.") + } + + return new Database(connString) + } + + useAsKvStore(): KvStore { + l.info`Creating Postgres key-value store...` + return new PostgresKvStore(this.sql, {}) + } + + async #doQueryFile(path: string, variables: Postgres.ParameterOrJSON[] = []): Promise> { + try { + l.debug`Running query at ${path}...` + var results = await this.sql.file(path, variables, undefined) + } + catch (e) { + l.error`Query at ${path} errored out with: ${e}` + throw e + } + l.debug`Query was successful, results are: ${results}` + return results + } + + async reinitializeDev() { + l.warn`Re-initializing database from scratch for development!!!` + + l.info`01/XX Dropping public schema from database...` + await this.#doQueryFile("src/database/init/01-drop-schema.sql") + + l.info`02/XX Recreating public schema...` + await this.#doQueryFile("src/database/init/02-create-schema.sql") + + l.info`03/XX Creating applied migrations table...` + await this.#doQueryFile("src/database/init/03-create-migration_applied.sql") + + l.info`04/XX Creating actor table...` + await this.#doQueryFile("src/database/init/04-create-actor.sql") + + l.info`05/XX Creating service table...` + await this.#doQueryFile("src/database/init/05-create-actor_service.sql") + + l.info`06/XX Creating player table...` + await this.#doQueryFile("src/database/init/06-create-actor_player.sql") + + l.info`07/XX Creating guild table...` + await this.#doQueryFile("src/database/init/07-create-actor_guild.sql") + + l.info`08/XX Creating service actor...` + l.debug`Creating RSA key pair...` + const {publicKey: publicRsa, privateKey: privateRsa} = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5") + l.debug`Creating elliptic curve key pair...` + const {publicKey: publicEd, privateKey: privateEd} = await generateCryptoKeyPair("Ed25519") + l.debug`Exporting to JWK format...` + const publicRsaJwk = await exportJwk(publicRsa) + const publicEdJwk = await exportJwk(publicEd) + const privateRsaJwk = await exportJwk(privateRsa) + const privateEdJwk = await exportJwk(privateEd) + l.debug`Running SQL query...` + await this.#doQueryFile( + "src/database/init/08-insert-actor_service.sql", + [ + publicRsaJwk, + publicEdJwk, + privateRsaJwk, + privateEdJwk, + ] + ) + + l.info("Done!") + } + + async getActorService(): Promise { + l.info`Getting service actor entity...` + + const results = await this.#doQueryFile("src/database/query/get-actor_service.sql") + + if(results.count == 0) { + l.error`No service actor entity exists.` + throw new Error("No service actor entity exists.") + } + else if(results.count > 1) { + l.error`Multiple service actor entities exist.` + throw new Error("Multiple service actor entities exist.") + } + + l.debug`Attempting to get only result...` + const result = results[0] + l.debug`Filtered down to result: ${result}` + + return result + } + + async getActorPlayer(steamId: number): Promise { + l.info`Getting player actor entity with id: ${steamId}` + + const results = await this.#doQueryFile("src/database/query/get-actor_player.sql", [steamId]) + + if(results.count == 0) { + l.debug`No such player actor entity exists.` + return null + } + else if(results.count > 1) { + l.error`Multiple player actor entities having the same SteamID exist.` + throw new Error("Multiple player actor entities having the same SteamID exist.") + } + + l.debug`Attempting to get only result...` + const result = results[0] + l.debug`Filtered down to result: ${result}` + + return result + } + + async getActorGuild(guildId: number): Promise { + l.info`Getting guild actor entity with id: ${guildId}` + + const results = await this.#doQueryFile("src/database/query/get-actor_guild.sql", [guildId]) + + if(results.count == 0) { + l.debug`No such guild actor entity exists.` + return null + } + else if(results.count > 1) { + l.error`Multiple guild actor entities having the same Guild ID exist.` + throw new Error("Multiple guild actor entities having the same Guild ID exist.") + } + + l.debug`Attempting to get only result...` + const result = results[0] + l.debug`Filtered down to result: ${result}` + + return result + } +} diff --git a/src/database/init/04-create-actor.sql b/src/database/init/04-create-actor.sql index cf073bc..a313e49 100644 --- a/src/database/init/04-create-actor.sql +++ b/src/database/init/04-create-actor.sql @@ -1,9 +1,3 @@ CREATE TABLE actor ( - handle VARCHAR PRIMARY KEY, - - public_rsassa BYTEA NOT NULL, - private_rsassa BYTEA NOT NULL, - - public_jwk BYTEA NOT NULL, - private_jwk BYTEA NOT NULL + handle VARCHAR PRIMARY KEY ); diff --git a/src/database/init/05-create-actor_service.sql b/src/database/init/05-create-actor_service.sql index e4318d7..e8c5c5e 100644 --- a/src/database/init/05-create-actor_service.sql +++ b/src/database/init/05-create-actor_service.sql @@ -1,5 +1,8 @@ CREATE TABLE actor_service ( - service_id SERIAL UNIQUE NOT NULL + public_rsa JSON NOT NULL, + public_ed JSON NOT NULL, + private_rsa JSON NOT NULL, + private_ed JSON NOT NULL ) INHERITS ( actor ); diff --git a/src/database/init/08-insert-actor_service.sql b/src/database/init/08-insert-actor_service.sql new file mode 100644 index 0000000..e44ae36 --- /dev/null +++ b/src/database/init/08-insert-actor_service.sql @@ -0,0 +1,13 @@ +INSERT INTO actor_service ( + handle, + public_rsa, + public_ed, + private_rsa, + private_ed +) VALUES ( + 'service', + $1, + $2, + $3, + $4 +); diff --git a/src/database/init/index.ts b/src/database/init/index.ts deleted file mode 100644 index a41c07a..0000000 --- a/src/database/init/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" -import { createPostgresFromEnv } from "../postgres.ts" -import { initLogging } from "../../deno/logging.ts" - - -const l = getLogger(["dotino-veloce", "database", "init"]) - - -async function main() { - await initLogging() - - l.debug`Creating Postgres instance...` - const postgres = createPostgresFromEnv() - - l.info`01/XX Dropping public schema from database...` - await postgres.file("src/database/init/01-drop-schema.sql") - - l.info`02/XX Recreating public schema...` - await postgres.file("src/database/init/02-create-schema.sql") - - l.info`03/XX Creating applied migrations table...` - await postgres.file("src/database/init/03-create-migration_applied.sql") - - l.info`04/XX Creating actor table...` - await postgres.file("src/database/init/04-create-actor.sql") - - l.info`05/XX Creating service table...` - await postgres.file("src/database/init/05-create-actor_service.sql") - - l.info`06/XX Creating player table...` - await postgres.file("src/database/init/06-create-actor_player.sql") - - l.info`07/XX Creating guild table...` - await postgres.file("src/database/init/07-create-actor_guild.sql") - - l.info("Done!") - Deno.exit(0) -} - - -await main() diff --git a/src/database/kv.ts b/src/database/kv.ts deleted file mode 100644 index 97d6aff..0000000 --- a/src/database/kv.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PostgresKvStore } from "@fedify/postgres/kv" -import { getLogger } from "@logtape/logtape" -import Postgres from "@@npm/postgres" - - -const l = getLogger(["dotino-veloce", "database", "kv"]) - - -export function createPostgresKvStore(postgres: Postgres.Sql): PostgresKvStore { - l.info`Creating Postgres key-value store...` - return new PostgresKvStore(postgres, {}) -} diff --git a/src/database/postgres.ts b/src/database/postgres.ts deleted file mode 100644 index 9380ad9..0000000 --- a/src/database/postgres.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Postgres from "@@npm/postgres" -import { getLogger } from "@logtape/logtape" - - -const l = getLogger(["dotino-veloce", "database", "postgres"]) - - -export function createPostgres(connString: string): Postgres.Sql { - l.info`Creating Postgres object with string: ${connString}` - return Postgres(connString) -} - -export function createPostgresFromEnv(): Postgres.Sql { - l.debug`Getting connection string from environment variable DOTINO_POSTGRES_STRING...` - const connString = Deno.env.get("DOTINO_POSTGRES_STRING") - if(!connString) { - l.error`DOTINO_POSTGRES_STRING is unset.` - throw new Error("DOTINO_POSTGRES_STRING is unset.") - } - - return createPostgres(connString) -} diff --git a/src/database/query/get-actor_guild.sql b/src/database/query/get-actor_guild.sql new file mode 100644 index 0000000..8c85971 --- /dev/null +++ b/src/database/query/get-actor_guild.sql @@ -0,0 +1,12 @@ +SELECT + actor_guild.handle, + actor_guild.guild_id, + actor_service.public_rsa, + actor_service.public_ed, + actor_service.private_rsa, + actor_service.private_ed +FROM + actor_guild, + actor_service +WHERE + actor_guild.steam_id = $1; diff --git a/src/database/query/get-actor_player.sql b/src/database/query/get-actor_player.sql new file mode 100644 index 0000000..6a65da9 --- /dev/null +++ b/src/database/query/get-actor_player.sql @@ -0,0 +1,12 @@ +SELECT + actor_player.handle, + actor_player.steam_id, + actor_service.public_rsa, + actor_service.public_ed, + actor_service.private_rsa, + actor_service.private_ed +FROM + actor_player, + actor_service +WHERE + actor_player.steam_id = $1; diff --git a/src/database/query/get-actor_service.sql b/src/database/query/get-actor_service.sql new file mode 100644 index 0000000..c1997bf --- /dev/null +++ b/src/database/query/get-actor_service.sql @@ -0,0 +1,8 @@ +SELECT + actor_service.handle, + actor_service.public_rsa, + actor_service.public_ed, + actor_service.private_rsa, + actor_service.private_ed +FROM + actor_service; diff --git a/src/dv/dotinoVeloce.ts b/src/dv/dotinoVeloce.ts index 64ad4ed..b172d2b 100644 --- a/src/dv/dotinoVeloce.ts +++ b/src/dv/dotinoVeloce.ts @@ -1,8 +1,9 @@ // deno-lint-ignore-file require-await -import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor, Follow, Endpoints } from "@fedify/fedify" +import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor, Follow, Endpoints, importSpki, importJwk } from "@fedify/fedify" import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" import { escapeHtml } from "@@x/escape" import { StratzAPI } from "../stratz/api.ts" +import { Database } from "../database/index.ts" const l = getLogger(["dotino-veloce", "ap", "federation"]) @@ -12,39 +13,39 @@ type ContextData = undefined export class DotinoVeloce { + db: Database stratz: StratzAPI federation: Federation - constructor(kv: KvStore, stratz: StratzAPI) { + constructor(db: Database, stratz: StratzAPI) { + this.db = db this.stratz = stratz - this.federation = createFederation({ kv }) + this.federation = createFederation({ kv: db.useAsKvStore() }) this.federation .setActorDispatcher("/users/{identifier}", this.#actorHandler.bind(this)) + .setKeyPairsDispatcher(this.#actorKeys.bind(this)) .mapHandle(this.#actorMapper.bind(this)) - // .setKeyPairsDispatcher(this.#actorKeys.bind(this)) this.federation .setInboxListeners("/inbox/{identifier}", "/inbox") .on(Follow, this.#followHandler.bind(this)) } - #commonActorProperties(ctx: Context, handle: string): Partial { + async #commonActorProperties(ctx: Context, handle: string): Promise> { l.debug`Generating common actor properties for ${handle}...` const properties = { + preferredUsername: handle, id: ctx.getActorUri(handle), + publicKeys: (await ctx.getActorKeyPairs(handle)).map(keyPair => keyPair.cryptographicKey), + assertionMethods: (await ctx.getActorKeyPairs(handle)).map(keyPair => keyPair.multikey), + // Akkoma requires inboxes to be setup to display profiles + // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/activity_pub/object_validators/user_validator.ex#L72 inbox: ctx.getInboxUri(handle), endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri(), }), - preferredUsername: handle, - // Akkoma expects URL to be equal to ID - // Or does it? This makes no sense to me... - // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/object/fetcher.ex#L287 - // url: id, - // Akkoma requires inboxes to be setup to display profiles - // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/activity_pub/object_validators/user_validator.ex#L72 } l.debug`Generated common actor properties for ${handle}: ${properties}` return properties @@ -79,7 +80,7 @@ export class DotinoVeloce { l.debug`Generating ActivityPub actor for player ${steamId}...` const actor = new Person({ - ...this.#commonActorProperties(ctx, handle), + ...(await this.#commonActorProperties(ctx, handle)), name: `[TEST] ${escapeHtml(player.name)}`, icon: new Image({ @@ -140,7 +141,7 @@ export class DotinoVeloce { l.debug`Generating ActivityPub actor for guild ${guildId}...` const actor = new Organization({ - ...this.#commonActorProperties(ctx, handle), + ...(await this.#commonActorProperties(ctx, handle)), name: `[TEST] ${escapeHtml(guild.name)}`, summary: escapeHtml(guild.description), @@ -181,7 +182,7 @@ export class DotinoVeloce { l.debug`Generating ActivityPub actor for service...` const actor = new Application({ - ...this.#commonActorProperties(ctx, handle), + ...(await this.#commonActorProperties(ctx, handle)), name: "[TEST] Dotino Veloce", summary: "Service account of a Dotino Veloce instance.", @@ -210,11 +211,41 @@ export class DotinoVeloce { } async #actorMapper(_ctx: Context, handle: string): Promise { + l.debug`Getting handle mapping for: ${handle}` return handle } - async #actorKeys(ctx: Context, handle: string): Promise { - throw "TODO: Not implemented" + async #actorKeys(_ctx: Context, handle: string): Promise { + l.debug`Getting keys for: ${handle}` + + l.debug`Requesting keys from the database...` + const { + public_rsa: publicRsaJwk, + public_ed: publicEdJwk, + private_rsa: privateRsaJwk, + private_ed: privateEdJwk, + } = await this.db.getActorService() + + l.debug`Importing keys from JWK format...` + const publicRsa = await importJwk(publicRsaJwk, "public") + const publicEd = await importJwk(publicEdJwk, "public") + const privateRsa = await importJwk(privateRsaJwk, "private") + const privateEd = await importJwk(privateEdJwk, "private") + + l.debug`Bundling keys...` + const result = [ + { + publicKey: publicRsa, + privateKey: privateRsa, + }, + { + publicKey: publicEd, + privateKey: privateEd, + } + ] + l.debug`Bundled keys are: ${result}` + + return result } async #followHandler(ctx: Context, follow: Follow) { diff --git a/src/entry/dbInit.ts b/src/entry/dbInit.ts new file mode 100644 index 0000000..a4220c0 --- /dev/null +++ b/src/entry/dbInit.ts @@ -0,0 +1,12 @@ +import { Database } from "../database/index.ts" +import { initLogging } from "../deno/logging.ts" + + +async function main() { + await initLogging() + const db = Database.fromEnv() + await db.reinitializeDev() +} + + +main() diff --git a/src/entry/server.ts b/src/entry/server.ts new file mode 100644 index 0000000..eb9bc68 --- /dev/null +++ b/src/entry/server.ts @@ -0,0 +1,21 @@ +import { doServe } from "../deno/server.ts" +import { initLogging } from "../deno/logging.ts" +import { StratzAPI } from "../stratz/api.ts" +import { behindProxy, Fetch } from "@hongminhee/x-forwarded-fetch" +import { createRouter } from "../deno/router.ts" +import { DotinoVeloce } from "../dv/dotinoVeloce.ts" +import { Database } from "../database/index.ts" + + +async function main() { + await initLogging() + const db = Database.fromEnv() + const stratz = StratzAPI.fromEnv() + const ap = new DotinoVeloce(db, stratz) + const router = createRouter(ap) + const routerBehindProxy = behindProxy(router as Fetch) + await doServe(routerBehindProxy) +} + + +main() diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index ca598a0..0000000 --- a/src/main.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { doServe } from "./deno/server.ts" -import { initLogging } from "./deno/logging.ts" -import { StratzAPI } from "./stratz/api.ts" -import { behindProxy, Fetch } from "@hongminhee/x-forwarded-fetch" -import { createRouter } from "./deno/router.ts" -import { DotinoVeloce } from "./dv/dotinoVeloce.ts" -import { createPostgresKvStore } from "./database/kv.ts" -import { createPostgresFromEnv } from "./database/postgres.ts" - - -async function main() { - await initLogging() - const postgres = createPostgresFromEnv() - const kv = createPostgresKvStore(postgres) - const stratz = StratzAPI.fromEnv() - const ap = new DotinoVeloce(kv, stratz) - const router = createRouter(ap) - const routerBehindProxy = behindProxy(router as Fetch) - await doServe(routerBehindProxy) -} - - -main() From 58d4e3b8a497e9de79023dfb511e2caad3120b88 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 21 Oct 2024 06:30:45 +0200 Subject: [PATCH 15/22] Authorized fetch, anyone? --- src/dv/dotinoVeloce.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/dv/dotinoVeloce.ts b/src/dv/dotinoVeloce.ts index b172d2b..4843169 100644 --- a/src/dv/dotinoVeloce.ts +++ b/src/dv/dotinoVeloce.ts @@ -27,6 +27,7 @@ export class DotinoVeloce { .setActorDispatcher("/users/{identifier}", this.#actorHandler.bind(this)) .setKeyPairsDispatcher(this.#actorKeys.bind(this)) .mapHandle(this.#actorMapper.bind(this)) + .authorize(this.#authorizationHandler.bind(this)) this.federation .setInboxListeners("/inbox/{identifier}", "/inbox") @@ -248,6 +249,10 @@ export class DotinoVeloce { return result } + async #authorizationHandler(_ctx: Context, _handle: string, _signedKey: unknown, _signedKeyOwner: unknown): Promise { + return true + } + async #followHandler(ctx: Context, follow: Follow) { l.info`Handling follow request: ${follow}` From 7d3ab00c2ac0da3ef1d6ee05081cb0b960d64502 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 22 Oct 2024 05:19:31 +0200 Subject: [PATCH 16/22] Various things --- .vscode/launch.json | 1 + src/deno/logging.ts | 2 +- src/deno/router.ts | 2 +- src/dv/favicon.ts | 2 +- src/dv/hostMeta.ts | 2 +- src/dv/{dotinoVeloce.ts => index.ts} | 45 ++++++++++++++++++++++++---- src/entry/server.ts | 2 +- 7 files changed, 45 insertions(+), 11 deletions(-) rename src/dv/{dotinoVeloce.ts => index.ts} (89%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 0b30c2a..b373eb9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -29,6 +29,7 @@ "cwd": "${workspaceFolder}", "envFile": "${workspaceFolder}/local.env", "runtimeExecutable": "/usr/bin/deno", + "restart": true, "runtimeArgs": [ "run", "--watch", diff --git a/src/deno/logging.ts b/src/deno/logging.ts index 866f0b7..d99cad1 100644 --- a/src/deno/logging.ts +++ b/src/deno/logging.ts @@ -10,7 +10,7 @@ export async function initLogging() { filters: {}, loggers: [ { category: ["logtape", "meta"], sinks: ["console"], level: "warning" }, - { category: ["fedify"], sinks: ["console"], level: "info" }, + { category: ["fedify"], sinks: ["console"], level: "debug" }, { category: ["dotino-veloce"], sinks: ["console"], level: "debug" }, ], }) diff --git a/src/deno/router.ts b/src/deno/router.ts index e905f8f..82715f5 100644 --- a/src/deno/router.ts +++ b/src/deno/router.ts @@ -1,6 +1,6 @@ import { getLogger } from "@logtape/logtape" import { handleHostMeta } from "../dv/hostMeta.ts" -import { DotinoVeloce } from "../dv/dotinoVeloce.ts" +import { DotinoVeloce } from "../dv/index.ts" import { handleFavicon } from "../dv/favicon.ts" diff --git a/src/dv/favicon.ts b/src/dv/favicon.ts index 04e9045..dc8eace 100644 --- a/src/dv/favicon.ts +++ b/src/dv/favicon.ts @@ -1,7 +1,7 @@ import { getLogger } from "@logtape/logtape" -const l = getLogger(["dotino-veloce", "ap", "favicon"]) +const l = getLogger(["dotino-veloce", "dv", "favicon"]) export async function handleFavicon(): Promise { diff --git a/src/dv/hostMeta.ts b/src/dv/hostMeta.ts index d7f6a1d..a706be4 100644 --- a/src/dv/hostMeta.ts +++ b/src/dv/hostMeta.ts @@ -1,7 +1,7 @@ import { getLogger } from "@logtape/logtape" -const l = getLogger(["dotino-veloce", "ap", "hostMeta"]) +const l = getLogger(["dotino-veloce", "dv", "hostMeta"]) // deno-lint-ignore require-await diff --git a/src/dv/dotinoVeloce.ts b/src/dv/index.ts similarity index 89% rename from src/dv/dotinoVeloce.ts rename to src/dv/index.ts index 4843169..bc39724 100644 --- a/src/dv/dotinoVeloce.ts +++ b/src/dv/index.ts @@ -1,12 +1,12 @@ // deno-lint-ignore-file require-await -import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor, Follow, Endpoints, importSpki, importJwk } from "@fedify/fedify" +import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor, Follow, Endpoints, importSpki, importJwk, NodeInfo } from "@fedify/fedify" import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" import { escapeHtml } from "@@x/escape" import { StratzAPI } from "../stratz/api.ts" import { Database } from "../database/index.ts" -const l = getLogger(["dotino-veloce", "ap", "federation"]) +const l = getLogger(["dotino-veloce", "dv", "index"]) type ContextData = undefined @@ -21,17 +21,50 @@ export class DotinoVeloce { this.db = db this.stratz = stratz - this.federation = createFederation({ kv: db.useAsKvStore() }) + this.federation = createFederation({ + kv: db.useAsKvStore(), + }) this.federation .setActorDispatcher("/users/{identifier}", this.#actorHandler.bind(this)) .setKeyPairsDispatcher(this.#actorKeys.bind(this)) .mapHandle(this.#actorMapper.bind(this)) - .authorize(this.#authorizationHandler.bind(this)) this.federation .setInboxListeners("/inbox/{identifier}", "/inbox") + // Akkoma with Authorized Fetch requires this to be set + // https://p.junimo.party/#/junimo.party/s/AnFW6s3OURPMY04LKq?view=full + .setSharedKeyDispatcher((_ctx: Context) => ({ identifier: "service" })) .on(Follow, this.#followHandler.bind(this)) + + this.federation + .setNodeInfoDispatcher("/nodeinfo/2.1", this.#nodeInfoHandler.bind(this)) + + // TODO: Setup a message queue + } + + async #nodeInfoHandler(ctx: Context): Promise { + return { + software: { + name: "dotino-veloce", + version: { + major: 0, + minor: 1, + patch: 0, + }, + repository: new URL("https://forge.steffo.eu/steffo/dotino-veloce"), + }, + protocols: ["activitypub"], + usage: { + users: { + total: 0, + activeHalfyear: 0, + activeMonth: 0, + }, + localPosts: 0, + localComments: 0, + } + } } async #commonActorProperties(ctx: Context, handle: string): Promise> { @@ -67,7 +100,7 @@ export class DotinoVeloce { l.debug`Handle ${handle}'s SteamID seems to be: ${steamId}` l.debug`Making sure the SteamID parsing didn't explode...` - if(Number.isFinite(steamId)) { + if(!Number.isFinite(steamId)) { l.error`SteamID parsing for ${handle} exploded with ${steamId}, returning null.` return null } @@ -105,7 +138,7 @@ export class DotinoVeloce { name: "DOTABUFF", value: `https://www.dotabuff.com/players/${player.id}`, }), - ] + ], }) l.debug`Generated ActivityPub actor for player ${steamId}: ${actor}` diff --git a/src/entry/server.ts b/src/entry/server.ts index eb9bc68..cf9bfc2 100644 --- a/src/entry/server.ts +++ b/src/entry/server.ts @@ -3,7 +3,7 @@ import { initLogging } from "../deno/logging.ts" import { StratzAPI } from "../stratz/api.ts" import { behindProxy, Fetch } from "@hongminhee/x-forwarded-fetch" import { createRouter } from "../deno/router.ts" -import { DotinoVeloce } from "../dv/dotinoVeloce.ts" +import { DotinoVeloce } from "../dv/index.ts" import { Database } from "../database/index.ts" From 5f61961792e79d99b9259c1eb9fef632b77c2f5a Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 22 Oct 2024 10:29:25 +0200 Subject: [PATCH 17/22] Commit, but this is broken on akkoma auth fetch --- .vscode/launch.json | 15 ++++++--------- src/deno/router.ts | 7 +++---- src/dv/index.ts | 13 ++++++++++--- src/entry/dbInit.ts | 4 ++-- src/entry/server.ts | 4 ++-- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index b373eb9..72c3ea5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,35 +8,32 @@ "request": "launch", "name": "Reinitialize database", "type": "node", - "program": "${workspaceFolder}/src/entry/dbInit.ts", - "cwd": "${workspaceFolder}", - "envFile": "${workspaceFolder}/local.env", "runtimeExecutable": "/usr/bin/deno", "runtimeArgs": [ "run", - "--watch", "--no-prompt", "--allow-all", "--inspect-wait" ], + "program": "${workspaceFolder}/src/entry/dbInit.ts", + "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/local.env", "attachSimplePort": 9229 }, { "request": "launch", "name": "Dev Server", "type": "node", - "program": "${workspaceFolder}/src/entry/server.ts", - "cwd": "${workspaceFolder}", - "envFile": "${workspaceFolder}/local.env", "runtimeExecutable": "/usr/bin/deno", - "restart": true, "runtimeArgs": [ "run", - "--watch", "--no-prompt", "--allow-all", "--inspect-wait" ], + "program": "${workspaceFolder}/src/entry/server.ts", + "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/local.env", "attachSimplePort": 9229 } ] diff --git a/src/deno/router.ts b/src/deno/router.ts index 82715f5..f2b5e21 100644 --- a/src/deno/router.ts +++ b/src/deno/router.ts @@ -12,16 +12,15 @@ export function createRouter(ap: DotinoVeloce) { l.debug`Determining request's User-Agent...` const agent = request.headers.get("User-Agent") - l.debug`Request's User-Agent is: ${agent}` l.debug`Determining request's URL...` const url = new URL(request.url) - l.debug`Request's URL is: ${url}` l.debug`Determining request's pathname...` const pathname = url.pathname - l.debug`Request's pathname is: ${pathname}` - + + l.info`User-Agent: ${agent} | URL: ${url} | Pathname: ${pathname}` + if (url.pathname === "/favicon.ico") { l.debug`Delegating handling to favicon handler...` return await handleFavicon() diff --git a/src/dv/index.ts b/src/dv/index.ts index bc39724..31170b3 100644 --- a/src/dv/index.ts +++ b/src/dv/index.ts @@ -22,7 +22,7 @@ export class DotinoVeloce { this.stratz = stratz this.federation = createFederation({ - kv: db.useAsKvStore(), + kv: db.useAsKvStore(), }) this.federation @@ -34,7 +34,7 @@ export class DotinoVeloce { .setInboxListeners("/inbox/{identifier}", "/inbox") // Akkoma with Authorized Fetch requires this to be set // https://p.junimo.party/#/junimo.party/s/AnFW6s3OURPMY04LKq?view=full - .setSharedKeyDispatcher((_ctx: Context) => ({ identifier: "service" })) + .setSharedKeyDispatcher(this.#instanceKeys.bind(this)) .on(Follow, this.#followHandler.bind(this)) this.federation @@ -43,7 +43,7 @@ export class DotinoVeloce { // TODO: Setup a message queue } - async #nodeInfoHandler(ctx: Context): Promise { + async #nodeInfoHandler(_ctx: Context): Promise { return { software: { name: "dotino-veloce", @@ -282,6 +282,11 @@ export class DotinoVeloce { return result } + async #instanceKeys(_ctx: Context): Promise<{ identifier: string }> { + l.debug`Getting instance keys...` + return {"identifier": "garasauto"} + } + async #authorizationHandler(_ctx: Context, _handle: string, _signedKey: unknown, _signedKeyOwner: unknown): Promise { return true } @@ -322,5 +327,7 @@ export class DotinoVeloce { l.debug`Attempting to determine target of the follow request...` const target = await follow.getTarget(ctx) l.debug`Target is: ${target}` + + } } diff --git a/src/entry/dbInit.ts b/src/entry/dbInit.ts index a4220c0..530e47e 100644 --- a/src/entry/dbInit.ts +++ b/src/entry/dbInit.ts @@ -2,11 +2,11 @@ import { Database } from "../database/index.ts" import { initLogging } from "../deno/logging.ts" -async function main() { +async function entryDbInit() { await initLogging() const db = Database.fromEnv() await db.reinitializeDev() } -main() +entryDbInit() diff --git a/src/entry/server.ts b/src/entry/server.ts index cf9bfc2..fca8b90 100644 --- a/src/entry/server.ts +++ b/src/entry/server.ts @@ -7,7 +7,7 @@ import { DotinoVeloce } from "../dv/index.ts" import { Database } from "../database/index.ts" -async function main() { +async function entryServer() { await initLogging() const db = Database.fromEnv() const stratz = StratzAPI.fromEnv() @@ -18,4 +18,4 @@ async function main() { } -main() +entryServer() From b29211c3878b9470c41616070e6e03b8f2bfecaf Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 22 Oct 2024 11:11:52 +0200 Subject: [PATCH 18/22] Sort of following now --- src/database/index.ts | 9 +++++++++ src/database/init/09-create-follow.sql | 9 +++++++++ src/database/query/delete-follow.sql | 6 ++++++ src/database/query/put-follow.sql | 7 +++++++ src/dv/index.ts | 28 +++++++++++++++++--------- 5 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 src/database/init/09-create-follow.sql create mode 100644 src/database/query/delete-follow.sql create mode 100644 src/database/query/put-follow.sql diff --git a/src/database/index.ts b/src/database/index.ts index 4bce443..1d7af3f 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -108,6 +108,9 @@ export class Database { ] ) + l.info`09/XX Creating follow table...` + await this.#doQueryFile("src/database/init/09-create-follow.sql") + l.info("Done!") } @@ -173,4 +176,10 @@ export class Database { return result } + + async putFollow(follower: string, followed: string): Promise { + l.info`Putting follow: from ${follower} to ${followed}` + + await this.#doQueryFile("src/database/query/put-follow.sql", [follower, followed]) + } } diff --git a/src/database/init/09-create-follow.sql b/src/database/init/09-create-follow.sql new file mode 100644 index 0000000..4864644 --- /dev/null +++ b/src/database/init/09-create-follow.sql @@ -0,0 +1,9 @@ +CREATE TABLE follow ( + follower VARCHAR REFERENCES (actor.handle) NOT NULL, + followed VARCHAR REFERENCES (actor.handle) NOT NULL, + + PRIMARY KEY ( + follower, + followed + ) +); diff --git a/src/database/query/delete-follow.sql b/src/database/query/delete-follow.sql new file mode 100644 index 0000000..6637479 --- /dev/null +++ b/src/database/query/delete-follow.sql @@ -0,0 +1,6 @@ +DELETE FROM + follow +WHERE + follow.follower = $1 + AND + follow.followed = $2; \ No newline at end of file diff --git a/src/database/query/put-follow.sql b/src/database/query/put-follow.sql new file mode 100644 index 0000000..c3f810a --- /dev/null +++ b/src/database/query/put-follow.sql @@ -0,0 +1,7 @@ +INSERT INTO follow ( + follower, + followed +) VALUES ( + $1, + $2 +) ON CONFLICT DO NOTHING; diff --git a/src/dv/index.ts b/src/dv/index.ts index 31170b3..082f040 100644 --- a/src/dv/index.ts +++ b/src/dv/index.ts @@ -1,5 +1,5 @@ // deno-lint-ignore-file require-await -import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor, Follow, Endpoints, importSpki, importJwk, NodeInfo } from "@fedify/fedify" +import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor, Follow, Endpoints, importSpki, importJwk, NodeInfo, Accept } from "@fedify/fedify" import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" import { escapeHtml } from "@@x/escape" import { StratzAPI } from "../stratz/api.ts" @@ -307,27 +307,37 @@ export class DotinoVeloce { return } - l.debug`Attempting to determine object of the follow request...` // TODO: ??? + l.debug`Attempting to determine what is being followed...` const object = ctx.parseUri(follow.objectId) l.debug`Object is: ${object}` if(!object) { - l.warn`Failed to determine object, skipping.` + l.warn`Failed to determine what is being followed, skipping.` return } if(object.type !== "actor") { - l.warn`Object type is not actor, skipping.` // TODO: Why? + l.warn`Attempting to follow something that is not actor, skipping.` return } - l.debug`Attempting to determine actor of the follow request...` + l.debug`Attempting to determine who sent the follow request...` const actor = await follow.getActor(ctx) l.debug`Actor is: ${actor}` - l.debug`Attempting to determine target of the follow request...` - const target = await follow.getTarget(ctx) - l.debug`Target is: ${target}` + if(!actor) { + l.warn`Failed to determine who sent the follow request, skipping.` + return + } + if(object.type !== "actor") { + l.warn`Being followed by someone who isn't an actor, skipping.` + return + } - + l.info`Accepting follow request from ${follow.actorId} to ${follow.objectId}...` + await ctx.sendActivity(object, "followers", new Accept({ + object: follow, + actor: follow.objectId, + to: follow.actorId, + })) } } From 0df82ac931d1485cc8fd561e19f28d857b8263ee Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Wed, 23 Oct 2024 03:44:20 +0200 Subject: [PATCH 19/22] Setup Postgres-based message queue --- src/database/index.ts | 8 +++++++- src/dv/index.ts | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/database/index.ts b/src/database/index.ts index 1d7af3f..cf24064 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -1,6 +1,7 @@ import Postgres from "@@npm/postgres" import { getLogger } from "@logtape/logtape" -import { exportJwk, generateCryptoKeyPair, KvStore } from "@fedify/fedify" +import { exportJwk, generateCryptoKeyPair, KvStore, MessageQueue } from "@fedify/fedify" +import { PostgresMessageQueue } from "@fedify/postgres" import { PostgresKvStore } from "@fedify/postgres/kv" @@ -50,6 +51,11 @@ export class Database { return new PostgresKvStore(this.sql, {}) } + useAsMessageQueue(): MessageQueue { + l.info`Creating Postgres message queue...` + return new PostgresMessageQueue(this.sql, {}) + } + async #doQueryFile(path: string, variables: Postgres.ParameterOrJSON[] = []): Promise> { try { l.debug`Running query at ${path}...` diff --git a/src/dv/index.ts b/src/dv/index.ts index 082f040..1b6da72 100644 --- a/src/dv/index.ts +++ b/src/dv/index.ts @@ -23,6 +23,7 @@ export class DotinoVeloce { this.federation = createFederation({ kv: db.useAsKvStore(), + queue: db.useAsMessageQueue(), }) this.federation From 548cfe139b1b62dc43abf7e5a3362b67b872762e Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Wed, 23 Oct 2024 03:47:37 +0200 Subject: [PATCH 20/22] Remove comment about message queues --- src/dv/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/dv/index.ts b/src/dv/index.ts index 1b6da72..ac11a11 100644 --- a/src/dv/index.ts +++ b/src/dv/index.ts @@ -40,8 +40,6 @@ export class DotinoVeloce { this.federation .setNodeInfoDispatcher("/nodeinfo/2.1", this.#nodeInfoHandler.bind(this)) - - // TODO: Setup a message queue } async #nodeInfoHandler(_ctx: Context): Promise { From 0f68d3ad145b63731366047002c5aaff9f3408bb Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Wed, 23 Oct 2024 03:50:34 +0200 Subject: [PATCH 21/22] Move nodeinfo to its own file --- src/dv/index.ts | 31 ++++--------------------------- src/dv/nodeinfo.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 src/dv/nodeinfo.ts diff --git a/src/dv/index.ts b/src/dv/index.ts index ac11a11..a0a851f 100644 --- a/src/dv/index.ts +++ b/src/dv/index.ts @@ -1,15 +1,16 @@ // deno-lint-ignore-file require-await -import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor, Follow, Endpoints, importSpki, importJwk, NodeInfo, Accept } from "@fedify/fedify" +import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, Context, Actor, Follow, Endpoints, importJwk, Accept } from "@fedify/fedify" import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" import { escapeHtml } from "@@x/escape" import { StratzAPI } from "../stratz/api.ts" import { Database } from "../database/index.ts" +import { handleNodeInfo } from "./nodeinfo.ts" const l = getLogger(["dotino-veloce", "dv", "index"]) -type ContextData = undefined +export type ContextData = undefined export class DotinoVeloce { @@ -39,31 +40,7 @@ export class DotinoVeloce { .on(Follow, this.#followHandler.bind(this)) this.federation - .setNodeInfoDispatcher("/nodeinfo/2.1", this.#nodeInfoHandler.bind(this)) - } - - async #nodeInfoHandler(_ctx: Context): Promise { - return { - software: { - name: "dotino-veloce", - version: { - major: 0, - minor: 1, - patch: 0, - }, - repository: new URL("https://forge.steffo.eu/steffo/dotino-veloce"), - }, - protocols: ["activitypub"], - usage: { - users: { - total: 0, - activeHalfyear: 0, - activeMonth: 0, - }, - localPosts: 0, - localComments: 0, - } - } + .setNodeInfoDispatcher("/nodeinfo/2.1", handleNodeInfo) } async #commonActorProperties(ctx: Context, handle: string): Promise> { diff --git a/src/dv/nodeinfo.ts b/src/dv/nodeinfo.ts new file mode 100644 index 0000000..f29e0af --- /dev/null +++ b/src/dv/nodeinfo.ts @@ -0,0 +1,33 @@ +import { Context, NodeInfo } from "@fedify/fedify" +import { ContextData } from "./index.ts" +import { getLogger } from "@logtape/logtape" + + +const l = getLogger(["dotino-veloce", "dv", "nodeinfo"]) + + +export function handleNodeInfo(_ctx: Context): NodeInfo { + l.info`Creating nodeinfo for...` + + return { + software: { + name: "dotino-veloce", + version: { + major: 0, + minor: 1, + patch: 0, + }, + repository: new URL("https://forge.steffo.eu/steffo/dotino-veloce"), + }, + protocols: ["activitypub"], + usage: { + users: { + total: 0, + activeHalfyear: 0, + activeMonth: 0, + }, + localPosts: 0, + localComments: 0, + } + } +} \ No newline at end of file From 569c4fbf324301b61f390f9c4c08cfeda346770e Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Wed, 23 Oct 2024 06:01:52 +0200 Subject: [PATCH 22/22] think i'm dropping this here for now --- src/database/index.ts | 66 ++-- src/database/init/03-create-actor.sql | 8 + .../init/03-create-migration_applied.sql | 3 - src/database/init/04-create-actor.sql | 3 - ...or_service.sql => 04-insert-actor-app.sql} | 4 +- src/database/init/05-create-actor_service.sql | 8 - src/database/init/05-create-follow.sql | 9 + src/database/init/06-create-actor_player.sql | 5 - src/database/init/07-create-actor_guild.sql | 5 - src/database/init/09-create-follow.sql | 9 - src/database/query/delete-follow.sql | 2 +- src/database/query/get-actor_app.sql | 10 + src/database/query/get-actor_guild.sql | 16 +- src/database/query/get-actor_player.sql | 16 +- src/database/query/get-actor_service.sql | 8 - src/dv/actor.ts | 196 +++++++++++ src/dv/component.ts | 15 + src/dv/follow.ts | 114 +++++++ src/dv/index.ts | 311 ++---------------- src/dv/key.ts | 68 ++++ src/dv/nodeinfo.ts | 46 +-- src/dv/undo.ts | 14 + 22 files changed, 527 insertions(+), 409 deletions(-) create mode 100644 src/database/init/03-create-actor.sql delete mode 100644 src/database/init/03-create-migration_applied.sql delete mode 100644 src/database/init/04-create-actor.sql rename src/database/init/{08-insert-actor_service.sql => 04-insert-actor-app.sql} (69%) delete mode 100644 src/database/init/05-create-actor_service.sql create mode 100644 src/database/init/05-create-follow.sql delete mode 100644 src/database/init/06-create-actor_player.sql delete mode 100644 src/database/init/07-create-actor_guild.sql delete mode 100644 src/database/init/09-create-follow.sql create mode 100644 src/database/query/get-actor_app.sql delete mode 100644 src/database/query/get-actor_service.sql create mode 100644 src/dv/actor.ts create mode 100644 src/dv/component.ts create mode 100644 src/dv/follow.ts create mode 100644 src/dv/key.ts create mode 100644 src/dv/undo.ts diff --git a/src/database/index.ts b/src/database/index.ts index cf24064..07053fd 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -16,16 +16,6 @@ export type DbActor = { private_ed: JsonWebKey, } -export type DbActorService = DbActor - -export type DbActorPlayer = DbActor & { - steam_id: bigint, -} - -export type DbActorGuild = DbActor & { - guild_id: number, -} - export class Database { sql: Postgres.Sql @@ -70,7 +60,7 @@ export class Database { } async reinitializeDev() { - l.warn`Re-initializing database from scratch for development!!!` + l.fatal`!!! Re-initializing database from scratch for development !!!` l.info`01/XX Dropping public schema from database...` await this.#doQueryFile("src/database/init/01-drop-schema.sql") @@ -78,22 +68,10 @@ export class Database { l.info`02/XX Recreating public schema...` await this.#doQueryFile("src/database/init/02-create-schema.sql") - l.info`03/XX Creating applied migrations table...` - await this.#doQueryFile("src/database/init/03-create-migration_applied.sql") + l.info`03/XX Creating actor table...` + await this.#doQueryFile("src/database/init/03-create-actor.sql") - l.info`04/XX Creating actor table...` - await this.#doQueryFile("src/database/init/04-create-actor.sql") - - l.info`05/XX Creating service table...` - await this.#doQueryFile("src/database/init/05-create-actor_service.sql") - - l.info`06/XX Creating player table...` - await this.#doQueryFile("src/database/init/06-create-actor_player.sql") - - l.info`07/XX Creating guild table...` - await this.#doQueryFile("src/database/init/07-create-actor_guild.sql") - - l.info`08/XX Creating service actor...` + l.info`04/XX Creating service actor...` l.debug`Creating RSA key pair...` const {publicKey: publicRsa, privateKey: privateRsa} = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5") l.debug`Creating elliptic curve key pair...` @@ -104,8 +82,8 @@ export class Database { const privateRsaJwk = await exportJwk(privateRsa) const privateEdJwk = await exportJwk(privateEd) l.debug`Running SQL query...` - await this.#doQueryFile( - "src/database/init/08-insert-actor_service.sql", + await this.#doQueryFile( + "src/database/init/04-insert-actor-app.sql", [ publicRsaJwk, publicEdJwk, @@ -114,24 +92,24 @@ export class Database { ] ) - l.info`09/XX Creating follow table...` - await this.#doQueryFile("src/database/init/09-create-follow.sql") + l.info`05/XX Creating follow table...` + await this.#doQueryFile("src/database/init/05-create-follow.sql") l.info("Done!") } - async getActorService(): Promise { - l.info`Getting service actor entity...` + async getActorApp(): Promise { + l.info`Getting app actor entity...` - const results = await this.#doQueryFile("src/database/query/get-actor_service.sql") + const results = await this.#doQueryFile("src/database/query/get-actor_app.sql") if(results.count == 0) { - l.error`No service actor entity exists.` - throw new Error("No service actor entity exists.") + l.error`No app actor entity exists.` + throw new Error("No app actor entity exists.") } else if(results.count > 1) { - l.error`Multiple service actor entities exist.` - throw new Error("Multiple service actor entities exist.") + l.error`Multiple app actor entities exist.` + throw new Error("Multiple app actor entities exist.") } l.debug`Attempting to get only result...` @@ -141,10 +119,10 @@ export class Database { return result } - async getActorPlayer(steamId: number): Promise { + async getActorPlayer(steamId: number): Promise { l.info`Getting player actor entity with id: ${steamId}` - const results = await this.#doQueryFile("src/database/query/get-actor_player.sql", [steamId]) + const results = await this.#doQueryFile("src/database/query/get-actor_player.sql", [steamId]) if(results.count == 0) { l.debug`No such player actor entity exists.` @@ -162,10 +140,10 @@ export class Database { return result } - async getActorGuild(guildId: number): Promise { + async getActorGuild(guildId: number): Promise { l.info`Getting guild actor entity with id: ${guildId}` - const results = await this.#doQueryFile("src/database/query/get-actor_guild.sql", [guildId]) + const results = await this.#doQueryFile("src/database/query/get-actor_guild.sql", [guildId]) if(results.count == 0) { l.debug`No such guild actor entity exists.` @@ -188,4 +166,10 @@ export class Database { await this.#doQueryFile("src/database/query/put-follow.sql", [follower, followed]) } + + async deleteFollow(follower: string, followed: string): Promise { + l.info`Deleting follow: from ${follower} to ${followed}` + + await this.#doQueryFile("src/database/query/delete-follow.sql", [follower, followed]) + } } diff --git a/src/database/init/03-create-actor.sql b/src/database/init/03-create-actor.sql new file mode 100644 index 0000000..1787ec0 --- /dev/null +++ b/src/database/init/03-create-actor.sql @@ -0,0 +1,8 @@ +CREATE TABLE actor ( + handle VARCHAR PRIMARY KEY, + + public_rsa JSON, + public_ed JSON, + private_rsa JSON, + private_ed JSON +); diff --git a/src/database/init/03-create-migration_applied.sql b/src/database/init/03-create-migration_applied.sql deleted file mode 100644 index 7aac154..0000000 --- a/src/database/init/03-create-migration_applied.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE TABLE migration_applied ( - code CHAR(4) PRIMARY KEY -); diff --git a/src/database/init/04-create-actor.sql b/src/database/init/04-create-actor.sql deleted file mode 100644 index a313e49..0000000 --- a/src/database/init/04-create-actor.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE TABLE actor ( - handle VARCHAR PRIMARY KEY -); diff --git a/src/database/init/08-insert-actor_service.sql b/src/database/init/04-insert-actor-app.sql similarity index 69% rename from src/database/init/08-insert-actor_service.sql rename to src/database/init/04-insert-actor-app.sql index e44ae36..5377df2 100644 --- a/src/database/init/08-insert-actor_service.sql +++ b/src/database/init/04-insert-actor-app.sql @@ -1,11 +1,11 @@ -INSERT INTO actor_service ( +INSERT INTO actor ( handle, public_rsa, public_ed, private_rsa, private_ed ) VALUES ( - 'service', + 'app', $1, $2, $3, diff --git a/src/database/init/05-create-actor_service.sql b/src/database/init/05-create-actor_service.sql deleted file mode 100644 index e8c5c5e..0000000 --- a/src/database/init/05-create-actor_service.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE actor_service ( - public_rsa JSON NOT NULL, - public_ed JSON NOT NULL, - private_rsa JSON NOT NULL, - private_ed JSON NOT NULL -) INHERITS ( - actor -); diff --git a/src/database/init/05-create-follow.sql b/src/database/init/05-create-follow.sql new file mode 100644 index 0000000..b465823 --- /dev/null +++ b/src/database/init/05-create-follow.sql @@ -0,0 +1,9 @@ +CREATE TABLE follow ( + follower VARCHAR NOT NULL, + followed VARCHAR REFERENCES actor (handle) NOT NULL, + + PRIMARY KEY ( + follower, + followed + ) +); diff --git a/src/database/init/06-create-actor_player.sql b/src/database/init/06-create-actor_player.sql deleted file mode 100644 index 8ff5637..0000000 --- a/src/database/init/06-create-actor_player.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE actor_player ( - steam_id BIGINT UNIQUE NOT NULL -) INHERITS ( - actor -); diff --git a/src/database/init/07-create-actor_guild.sql b/src/database/init/07-create-actor_guild.sql deleted file mode 100644 index 62910d6..0000000 --- a/src/database/init/07-create-actor_guild.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE actor_guild ( - guild_id INT UNIQUE NOT NULL -) INHERITS ( - actor -); diff --git a/src/database/init/09-create-follow.sql b/src/database/init/09-create-follow.sql deleted file mode 100644 index 4864644..0000000 --- a/src/database/init/09-create-follow.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE follow ( - follower VARCHAR REFERENCES (actor.handle) NOT NULL, - followed VARCHAR REFERENCES (actor.handle) NOT NULL, - - PRIMARY KEY ( - follower, - followed - ) -); diff --git a/src/database/query/delete-follow.sql b/src/database/query/delete-follow.sql index 6637479..f0f7e9d 100644 --- a/src/database/query/delete-follow.sql +++ b/src/database/query/delete-follow.sql @@ -3,4 +3,4 @@ DELETE FROM WHERE follow.follower = $1 AND - follow.followed = $2; \ No newline at end of file + follow.followed = $2; diff --git a/src/database/query/get-actor_app.sql b/src/database/query/get-actor_app.sql new file mode 100644 index 0000000..21ea7dd --- /dev/null +++ b/src/database/query/get-actor_app.sql @@ -0,0 +1,10 @@ +SELECT + actor.handle, + actor.public_rsa, + actor.public_ed, + actor.private_rsa, + actor.private_ed +FROM + actor +WHERE + actor.handle = 'app'; diff --git a/src/database/query/get-actor_guild.sql b/src/database/query/get-actor_guild.sql index 8c85971..af37678 100644 --- a/src/database/query/get-actor_guild.sql +++ b/src/database/query/get-actor_guild.sql @@ -1,12 +1,10 @@ SELECT - actor_guild.handle, - actor_guild.guild_id, - actor_service.public_rsa, - actor_service.public_ed, - actor_service.private_rsa, - actor_service.private_ed + actor.handle, + actor.public_rsa, + actor.public_ed, + actor.private_rsa, + actor.private_ed FROM - actor_guild, - actor_service + actor WHERE - actor_guild.steam_id = $1; + actor.handle = 'g' || $1; diff --git a/src/database/query/get-actor_player.sql b/src/database/query/get-actor_player.sql index 6a65da9..43b845b 100644 --- a/src/database/query/get-actor_player.sql +++ b/src/database/query/get-actor_player.sql @@ -1,12 +1,10 @@ SELECT - actor_player.handle, - actor_player.steam_id, - actor_service.public_rsa, - actor_service.public_ed, - actor_service.private_rsa, - actor_service.private_ed + actor.handle, + actor.public_rsa, + actor.public_ed, + actor.private_rsa, + actor.private_ed FROM - actor_player, - actor_service + actor WHERE - actor_player.steam_id = $1; + actor.handle = 'u' || $1; diff --git a/src/database/query/get-actor_service.sql b/src/database/query/get-actor_service.sql deleted file mode 100644 index c1997bf..0000000 --- a/src/database/query/get-actor_service.sql +++ /dev/null @@ -1,8 +0,0 @@ -SELECT - actor_service.handle, - actor_service.public_rsa, - actor_service.public_ed, - actor_service.private_rsa, - actor_service.private_ed -FROM - actor_service; diff --git a/src/dv/actor.ts b/src/dv/actor.ts new file mode 100644 index 0000000..0944175 --- /dev/null +++ b/src/dv/actor.ts @@ -0,0 +1,196 @@ +import { Actor, Application, Context, Endpoints, Image, Organization, Person, PropertyValue } from "@fedify/fedify" +import { ContextData } from "./index.ts" +import { getLogger } from "@logtape/logtape" +import { escapeHtml } from "@@x/escape" +import { DvComponent } from "./component.ts" + + +const l = getLogger(["dotino-veloce", "dv", "actor", "common"]) +const lApp = getLogger(["dotino-veloce", "dv", "actor", "app"]) +const lPlayer = getLogger(["dotino-veloce", "dv", "actor", "player"]) +const lGuild = getLogger(["dotino-veloce", "dv", "actor", "guild"]) + + +export class ActorComponent extends DvComponent { + static appMatcher = /app/ + static playerMatcher = /u([0-9]+)/ + static guildMatcher = /g([0-9]+)/ + + async getActor(ctx: Context, handle: string): Promise { + l.info`Getting actor: ${handle}` + + let actor = null + + actor ??= await this.getAppActor(ctx, handle) + actor ??= await this.getPlayerActor(ctx, handle) + actor ??= await this.getGuildActor(ctx, handle) + + l.debug`Obtained actor: ${handle}` + + return actor + } + + async getCommonActorProperties(ctx: Context, handle: string): Promise> { + l.info`Generating common actor properties for ${handle}...` + const properties = { + preferredUsername: handle, + id: ctx.getActorUri(handle), + + // This is required for compatibility with a whole lot of servers + publicKeys: (await ctx.getActorKeyPairs(handle)).map(keyPair => keyPair.cryptographicKey), + assertionMethods: (await ctx.getActorKeyPairs(handle)).map(keyPair => keyPair.multikey), + + // Akkoma requires inboxes to be setup to display profiles + // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/activity_pub/object_validators/user_validator.ex#L72 + inbox: ctx.getInboxUri(handle), + endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), + }), + } + l.debug`Generated common actor properties for ${handle}: ${properties}` + return properties + } + + async getAppActor(ctx: Context, handle: string): Promise { + lApp.debug`Checking if handle ${handle} matches regex for the app account...` + const match = handle.match(ActorComponent.appMatcher) + if(!match) { + lApp.debug`No match for app account regex, returning null.` + return null + } + lApp.info`Attempting to generate ActivityPub actor for app...` + + lApp.debug`Creating ActivityPub actor for app...` + const actor = new Application({ + ...(await this.getCommonActorProperties(ctx, handle)), + + name: "[TEST] Dotino Veloce", + summary: "App account of a Dotino Veloce instance.", + attachments: [ + new PropertyValue({ + name: "Source code", + value: `https://forge.steffo.eu/steffo/dotino-veloce`, + }), + ] + }) + + lApp.debug`Generated ActivityPub actor for service: ${actor}` + return actor + } + + async getPlayerActor(ctx: Context, handle: string): Promise { + lPlayer.debug`Checking if handle ${handle} matches regex for player accounts...` + const match = handle.match(ActorComponent.playerMatcher) + if(!match) { + lPlayer.debug`No match for player account regex, returning null.` + return null + } + lApp.info`Attempting to generate ActivityPub actor for player...` + + lPlayer.debug`Getting handle ${handle}'s SteamID...` + const steamId = Number.parseInt(match[1] as any) + lPlayer.debug`Handle ${handle}'s SteamID seems to be: ${steamId}` + + lPlayer.debug`Making sure the SteamID parsing didn't explode...` + if(!Number.isFinite(steamId)) { + lPlayer.error`SteamID parsing for ${handle} exploded with ${steamId}, returning null.` + return null + } + + lPlayer.debug`Getting player data for ${steamId}...` + const player = await this.dv.stratz.doQueryPlayer(steamId) + if(player === null) { + lPlayer.debug`No Steam account was found with ID: ${steamId}` + return null + } + + lPlayer.debug`Creating ActivityPub actor for player ${steamId}...` + const actor = new Person({ + ...(await this.getCommonActorProperties(ctx, handle)), + + name: `[TEST] ${escapeHtml(player.name)}`, + icon: new Image({ + url: new URL(player.avatar), + mediaType: "image/jpeg" + }), + attachments: [ + new PropertyValue({ + name: "Steam", + value: `https://steamcommunity.com/profiles/[U:1:${player.id}]`, + }), + new PropertyValue({ + name: "STRATZ", + value: `https://stratz.com/players/${player.id}`, + }), + new PropertyValue({ + name: "OpenDota", + value: `https://www.opendota.com/players/${player.id}`, + }), + new PropertyValue({ + name: "DOTABUFF", + value: `https://www.dotabuff.com/players/${player.id}`, + }), + ], + }) + lPlayer.debug`Generated ActivityPub actor for player ${steamId}: ${actor}` + + return actor + } + + async getGuildActor(ctx: Context, handle: string): Promise { + lGuild.debug`Checking if handle ${handle} matches regex for guild accounts...` + const match = handle.match(ActorComponent.guildMatcher) + if(!match) { + lGuild.debug`No match for guild account regex, returning null.` + return null + } + lApp.info`Attempting to generate ActivityPub actor for guild...` + + lGuild.debug`Getting handle ${handle}'s Guild ID...` + const guildId = Number.parseInt(match[1] as any) + lGuild.debug`Handle ${handle}'s Guild ID seems to be: ${guildId}` + + lGuild.debug`Making sure the Guild ID parsing didn't explode...` + if(Number.isFinite(guildId)) { + lGuild.error`Guild ID parsing for ${handle} exploded with ${guildId}, returning null.` + return null + } + + lPlayer.debug`Getting guild data for ${guildId}...` + const guild = await this.dv.stratz.doQueryGuild(guildId) + if(guild === null) { + lGuild.warn`No guild was found with ID: ${guildId}` + return null + } + + lGuild.debug`Creating ActivityPub actor for guild ${guildId}...` + const actor = new Organization({ + ...(await this.getCommonActorProperties(ctx, handle)), + + name: `[TEST] ${escapeHtml(guild.name)}`, + summary: escapeHtml(guild.description), + icon: new Image({ + url: new URL(`https://steamusercontent-a.akamaihd.net/ugc/${guild.logo}`), + mediaType: "image/jpeg" + }), + published: Temporal.Instant.fromEpochMilliseconds(guild.createdDateTime * 1000), + attachments: [ + new PropertyValue({ + name: "Tag", + value: `[${escapeHtml(guild.tag)}]`, + }), + new PropertyValue({ + name: "Message of the day", + value: escapeHtml(guild.motd), + }), + new PropertyValue({ + name: "STRATZ", + value: `https://stratz.com/guilds/${guild.id}`, + }), + ] + }) + lGuild.debug`Generated ActivityPub actor for guild ${guildId}: ${actor}` + + return actor + } +} \ No newline at end of file diff --git a/src/dv/component.ts b/src/dv/component.ts new file mode 100644 index 0000000..2b7476a --- /dev/null +++ b/src/dv/component.ts @@ -0,0 +1,15 @@ +import { getLogger } from "@logtape/logtape" +import { DotinoVeloce } from "./index.ts" + + +const l = getLogger(["dotino-veloce", "dv", "component"]) + + +export class DvComponent { + dv: DotinoVeloce + + constructor(dv: DotinoVeloce) { + l.debug`Instantiating DvComponent...` + this.dv = dv + } +} diff --git a/src/dv/follow.ts b/src/dv/follow.ts new file mode 100644 index 0000000..380b63a --- /dev/null +++ b/src/dv/follow.ts @@ -0,0 +1,114 @@ +import { getLogger } from "@logtape/logtape" +import { ContextData } from "./index.ts" +import { Accept, Context, Follow, Reject } from "@fedify/fedify" +import { DvComponent } from "./component.ts" + + +const lPlus = getLogger(["dotino-veloce", "dv", "follow", "plus"]) +const lMinus = getLogger(["dotino-veloce", "dv", "follow", "minus"]) + + +export class FollowComponent extends DvComponent { + async handlePlus(ctx: Context, follow: Follow) { + lPlus.debug`Received Follow activity: ${follow}` + + if(!follow.id) { + lPlus.warn`Missing follow ID, skipping.` + return + } + if(!follow.actorId) { + lPlus.warn`Missing actor ID, skipping.` + return + } + if(!follow.objectId) { + lPlus.warn`Missing object ID, skipping.` + return + } + + lPlus.debug`Attempting to determine who sent the follow request...` + const actor = await follow.getActor(ctx) + + if(!actor) { + lPlus.warn`Failed to determine who sent the follow request, skipping.` + return + } + + lPlus.debug`Attempting to determine who is being followed...` + const object = ctx.parseUri(follow.objectId) + + if(!object) { + lPlus.warn`Failed to determine who is being followed, skipping.` + return + } + if(object.type !== "actor") { + lPlus.warn`Attempting to follow something that is not actor, skipping.` + return + } + + lPlus.info`Confirming follow request from ${follow.actorId} to ${follow.objectId}...` + + lPlus.debug`Storing follow information in the database...` + this.dv.db.putFollow(follow.actorId.toString(), follow.objectId.toString()) + + lPlus.debug`Sending follow Accept activity...` + await ctx.sendActivity(object, "followers", new Accept({ + object: follow, + actor: follow.objectId, + to: follow.actorId, + })) + + lPlus.debug`Successfully accepted follow request!` + } + + async handleMinus(ctx: Context, follow: Follow) { + lMinus.debug`Received Undo Follow activity: ${follow}` + + if(!follow.id) { + lMinus.warn`Missing follow ID, skipping.` + return + } + if(!follow.actorId) { + lMinus.warn`Missing actor ID, skipping.` + return + } + if(!follow.objectId) { + lMinus.warn`Missing object ID, skipping.` + return + } + + lMinus.debug`Attempting to determine who sent the follow request...` + const actor = await follow.getActor(ctx) + + if(!actor) { + lMinus.warn`Failed to determine who sent the follow request, skipping.` + return + } + + lMinus.debug`Attempting to determine who is being followed...` + const object = ctx.parseUri(follow.objectId) + + if(!object) { + lMinus.warn`Failed to determine who is being followed, skipping.` + return + } + if(object.type !== "actor") { + lMinus.warn`Attempting to follow something that is not actor, skipping.` + return + } + + lMinus.info`Undoing follow request from ${follow.actorId} to ${follow.objectId}...` + + lMinus.debug`Deleting follow information from the database...` + this.dv.db.deleteFollow(follow.actorId.toString(), follow.objectId.toString()) + + // Do I really need to do this? + lMinus.debug`Sending follow Reject activity...` + await ctx.sendActivity(object, "followers", new Reject({ + object: follow, + actor: follow.objectId, + to: follow.actorId, + })) + + lMinus.debug`Successfully undone follow request!` + } +} diff --git a/src/dv/index.ts b/src/dv/index.ts index a0a851f..5cd82b8 100644 --- a/src/dv/index.ts +++ b/src/dv/index.ts @@ -1,10 +1,12 @@ -// deno-lint-ignore-file require-await -import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, Context, Actor, Follow, Endpoints, importJwk, Accept } from "@fedify/fedify" +import { createFederation, Federation, Follow, Undo } from "@fedify/fedify" import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" -import { escapeHtml } from "@@x/escape" import { StratzAPI } from "../stratz/api.ts" import { Database } from "../database/index.ts" -import { handleNodeInfo } from "./nodeinfo.ts" +import { NodeInfoComponent } from "./nodeinfo.ts" +import { ActorComponent } from "./actor.ts" +import { KeyComponent } from "./key.ts" +import { FollowComponent } from "./follow.ts" +import { UndoComponent } from "./undo.ts" const l = getLogger(["dotino-veloce", "dv", "index"]) @@ -18,9 +20,21 @@ export class DotinoVeloce { stratz: StratzAPI federation: Federation + cNodeInfo: NodeInfoComponent + cActor: ActorComponent + cKey: KeyComponent + cFollow: FollowComponent + cUndo: UndoComponent + constructor(db: Database, stratz: StratzAPI) { this.db = db this.stratz = stratz + + this.cNodeInfo = new NodeInfoComponent(this) + this.cActor = new ActorComponent(this) + this.cKey = new KeyComponent(this) + this.cFollow = new FollowComponent(this) + this.cUndo = new UndoComponent(this) this.federation = createFederation({ kv: db.useAsKvStore(), @@ -28,292 +42,21 @@ export class DotinoVeloce { }) this.federation - .setActorDispatcher("/users/{identifier}", this.#actorHandler.bind(this)) - .setKeyPairsDispatcher(this.#actorKeys.bind(this)) - .mapHandle(this.#actorMapper.bind(this)) + .setActorDispatcher("/users/{identifier}", this.cActor.getActor.bind(this.cActor)) + .setKeyPairsDispatcher(this.cKey.getActorKeys.bind(this.cKey)) + .mapHandle((_, h) => h) this.federation .setInboxListeners("/inbox/{identifier}", "/inbox") // Akkoma with Authorized Fetch requires this to be set // https://p.junimo.party/#/junimo.party/s/AnFW6s3OURPMY04LKq?view=full - .setSharedKeyDispatcher(this.#instanceKeys.bind(this)) - .on(Follow, this.#followHandler.bind(this)) + .setSharedKeyDispatcher(this.cKey.getFetchKey.bind(this.cKey)) + .on(Follow, this.cFollow.handlePlus.bind(this.cFollow)) + .on(Undo, this.cUndo.handle.bind(this.cUndo)) this.federation - .setNodeInfoDispatcher("/nodeinfo/2.1", handleNodeInfo) + .setNodeInfoDispatcher("/nodeinfo/2.1", this.cNodeInfo.handle.bind(this.cNodeInfo)) } - async #commonActorProperties(ctx: Context, handle: string): Promise> { - l.debug`Generating common actor properties for ${handle}...` - const properties = { - preferredUsername: handle, - id: ctx.getActorUri(handle), - publicKeys: (await ctx.getActorKeyPairs(handle)).map(keyPair => keyPair.cryptographicKey), - assertionMethods: (await ctx.getActorKeyPairs(handle)).map(keyPair => keyPair.multikey), - // Akkoma requires inboxes to be setup to display profiles - // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/activity_pub/object_validators/user_validator.ex#L72 - inbox: ctx.getInboxUri(handle), - endpoints: new Endpoints({ - sharedInbox: ctx.getInboxUri(), - }), - } - l.debug`Generated common actor properties for ${handle}: ${properties}` - return properties - } - - static playerMatcher = /u([0-9]+)/ - - async #playerActor(ctx: Context, handle: string): Promise { - l.debug`Checking if handle ${handle} matches regex for player accounts...` - const match = handle.match(DotinoVeloce.playerMatcher) - if(!match) { - l.debug`No match for player account regex, returning null.` - return null - } - - l.debug`Getting handle ${handle}'s SteamID...` - const steamId = Number.parseInt(match[1] as any) - l.debug`Handle ${handle}'s SteamID seems to be: ${steamId}` - - l.debug`Making sure the SteamID parsing didn't explode...` - if(!Number.isFinite(steamId)) { - l.error`SteamID parsing for ${handle} exploded with ${steamId}, returning null.` - return null - } - - l.debug`Getting player data for ${steamId}...` - const player = await this.stratz.doQueryPlayer(steamId) - if(player === null) { - l.debug`No Steam account was found with ID: ${steamId}` - return null - } - - l.debug`Generating ActivityPub actor for player ${steamId}...` - const actor = new Person({ - ...(await this.#commonActorProperties(ctx, handle)), - - name: `[TEST] ${escapeHtml(player.name)}`, - icon: new Image({ - url: new URL(player.avatar), - mediaType: "image/jpeg" - }), - attachments: [ - new PropertyValue({ - name: "Steam", - value: `https://steamcommunity.com/profiles/[U:1:${player.id}]`, - }), - new PropertyValue({ - name: "STRATZ", - value: `https://stratz.com/players/${player.id}`, - }), - new PropertyValue({ - name: "OpenDota", - value: `https://www.opendota.com/players/${player.id}`, - }), - new PropertyValue({ - name: "DOTABUFF", - value: `https://www.dotabuff.com/players/${player.id}`, - }), - ], - }) - l.debug`Generated ActivityPub actor for player ${steamId}: ${actor}` - - return actor - } - - static guildMatcher = /g([0-9]+)/ - - async #guildActor(ctx: Context, handle: string): Promise { - l.debug`Checking if handle ${handle} matches regex for guild accounts...` - const match = handle.match(DotinoVeloce.guildMatcher) - if(!match) { - l.debug`No match for guild account regex, returning null.` - return null - } - - l.debug`Getting handle ${handle}'s Guild ID...` - const guildId = Number.parseInt(match[1] as any) - l.debug`Handle ${handle}'s Guild ID seems to be: ${guildId}` - - l.debug`Making sure the Guild ID parsing didn't explode...` - if(Number.isFinite(guildId)) { - l.error`Guild ID parsing for ${handle} exploded with ${guildId}, returning null.` - return null - } - - - const guild = await this.stratz.doQueryGuild(guildId) - - if(guild === null) { - l.warn`No guild was found with ID: ${guildId}` - return null - } - - l.debug`Generating ActivityPub actor for guild ${guildId}...` - const actor = new Organization({ - ...(await this.#commonActorProperties(ctx, handle)), - - name: `[TEST] ${escapeHtml(guild.name)}`, - summary: escapeHtml(guild.description), - icon: new Image({ - url: new URL(`https://steamusercontent-a.akamaihd.net/ugc/${guild.logo}`), - mediaType: "image/jpeg" - }), - published: Temporal.Instant.fromEpochMilliseconds(guild.createdDateTime * 1000), - attachments: [ - new PropertyValue({ - name: "Tag", - value: `[${escapeHtml(guild.tag)}]`, - }), - new PropertyValue({ - name: "Message of the day", - value: escapeHtml(guild.motd), - }), - new PropertyValue({ - name: "STRATZ", - value: `https://stratz.com/guilds/${guild.id}`, - }), - ] - }) - l.debug`Generated ActivityPub actor for guild ${guildId}: ${actor}` - - return actor - } - - static serviceMatcher = /service/ - - async #serviceActor(ctx: Context, handle: string): Promise { - l.debug`Checking if handle ${handle} matches regex for the service account...` - const match = handle.match(DotinoVeloce.serviceMatcher) - if(!match) { - l.debug`No match for service account regex, returning null.` - return null - } - - l.debug`Generating ActivityPub actor for service...` - const actor = new Application({ - ...(await this.#commonActorProperties(ctx, handle)), - - name: "[TEST] Dotino Veloce", - summary: "Service account of a Dotino Veloce instance.", - attachments: [ - new PropertyValue({ - name: "Source code", - value: `https://forge.steffo.eu/steffo/dotino-veloce`, - }), - ] - }) - - l.debug`Generated ActivityPub actor for service: ${actor}` - return actor - } - - async #actorHandler(ctx: Context, handle: string): Promise { - l.info`Handling actor with handle: ${handle}` - - let actor = null - - actor ??= await this.#serviceActor(ctx, handle) - actor ??= await this.#playerActor(ctx, handle) - actor ??= await this.#guildActor(ctx, handle) - - return actor - } - - async #actorMapper(_ctx: Context, handle: string): Promise { - l.debug`Getting handle mapping for: ${handle}` - return handle - } - - async #actorKeys(_ctx: Context, handle: string): Promise { - l.debug`Getting keys for: ${handle}` - - l.debug`Requesting keys from the database...` - const { - public_rsa: publicRsaJwk, - public_ed: publicEdJwk, - private_rsa: privateRsaJwk, - private_ed: privateEdJwk, - } = await this.db.getActorService() - - l.debug`Importing keys from JWK format...` - const publicRsa = await importJwk(publicRsaJwk, "public") - const publicEd = await importJwk(publicEdJwk, "public") - const privateRsa = await importJwk(privateRsaJwk, "private") - const privateEd = await importJwk(privateEdJwk, "private") - - l.debug`Bundling keys...` - const result = [ - { - publicKey: publicRsa, - privateKey: privateRsa, - }, - { - publicKey: publicEd, - privateKey: privateEd, - } - ] - l.debug`Bundled keys are: ${result}` - - return result - } - - async #instanceKeys(_ctx: Context): Promise<{ identifier: string }> { - l.debug`Getting instance keys...` - return {"identifier": "garasauto"} - } - - async #authorizationHandler(_ctx: Context, _handle: string, _signedKey: unknown, _signedKeyOwner: unknown): Promise { - return true - } - - async #followHandler(ctx: Context, follow: Follow) { - l.info`Handling follow request: ${follow}` - - if(!follow.id) { - l.warn`Missing follow ID, skipping.` - return - } - if(!follow.actorId) { - l.warn`Missing actor ID, skipping.` - return - } - if(!follow.objectId) { - l.warn`Missing object ID, skipping.` - return - } - - l.debug`Attempting to determine what is being followed...` - const object = ctx.parseUri(follow.objectId) - l.debug`Object is: ${object}` - - if(!object) { - l.warn`Failed to determine what is being followed, skipping.` - return - } - if(object.type !== "actor") { - l.warn`Attempting to follow something that is not actor, skipping.` - return - } - - l.debug`Attempting to determine who sent the follow request...` - const actor = await follow.getActor(ctx) - l.debug`Actor is: ${actor}` - - if(!actor) { - l.warn`Failed to determine who sent the follow request, skipping.` - return - } - if(object.type !== "actor") { - l.warn`Being followed by someone who isn't an actor, skipping.` - return - } - - l.info`Accepting follow request from ${follow.actorId} to ${follow.objectId}...` - await ctx.sendActivity(object, "followers", new Accept({ - object: follow, - actor: follow.objectId, - to: follow.actorId, - })) - } -} + // REMINDER: A pattern matcher on received activities could be useful, actually. +} \ No newline at end of file diff --git a/src/dv/key.ts b/src/dv/key.ts new file mode 100644 index 0000000..ccdbeec --- /dev/null +++ b/src/dv/key.ts @@ -0,0 +1,68 @@ +import { Context, importJwk, SenderKeyPair } from "@fedify/fedify" +import { ContextData } from "./index.ts" +import { getLogger } from "@logtape/logtape" +import { DvComponent } from "./component.ts" + + +const lActor = getLogger(["dotino-veloce", "dv", "key", "actor"]) +const lFetch = getLogger(["dotino-veloce", "dv", "key", "fetch"]) + + +export class KeyComponent extends DvComponent { + async getActorKeys(_ctx: Context, handle: string): Promise { + lActor.info`Getting actor keys for: ${handle}` + + lActor.debug`Requesting keys from the database...` + const { + public_rsa: publicRsaJwk, + public_ed: publicEdJwk, + private_rsa: privateRsaJwk, + private_ed: privateEdJwk, + } = await this.dv.db.getActorApp() + + lActor.debug`Importing keys from JWK format...` + const publicRsa = await importJwk(publicRsaJwk, "public") + const publicEd = await importJwk(publicEdJwk, "public") + const privateRsa = await importJwk(privateRsaJwk, "private") + const privateEd = await importJwk(privateEdJwk, "private") + + lActor.debug`Bundling keys...` + const result = [ + { + publicKey: publicRsa, + privateKey: privateRsa, + }, + { + publicKey: publicEd, + privateKey: privateEd, + } + ] + lActor.debug`Bundled keys are: ${result}` + + return result + } + + async getFetchKey(ctx: Context): Promise { + lFetch.info`Getting fetch key...` + + lFetch.debug`Requesting key from the database...` + const { + private_rsa: privateKeyJwk, + } = await this.dv.db.getActorApp() + + lFetch.debug`Importing key from JWK format...` + const privateKey = await importJwk(privateKeyJwk, "private") + + lFetch.debug`Getting app actor URI...` + const keyId = ctx.getActorUri("app") + + lFetch.debug`Appending #main-key to the app actor URI...` + keyId.hash = "main-key" + + lFetch.debug`Bundling key...` + const result = {privateKey, keyId} + lFetch.debug`Bundled key is: ${result}` + + return result + } +} \ No newline at end of file diff --git a/src/dv/nodeinfo.ts b/src/dv/nodeinfo.ts index f29e0af..c6945aa 100644 --- a/src/dv/nodeinfo.ts +++ b/src/dv/nodeinfo.ts @@ -1,33 +1,35 @@ import { Context, NodeInfo } from "@fedify/fedify" import { ContextData } from "./index.ts" import { getLogger } from "@logtape/logtape" +import { DvComponent } from "./component.ts" const l = getLogger(["dotino-veloce", "dv", "nodeinfo"]) -export function handleNodeInfo(_ctx: Context): NodeInfo { - l.info`Creating nodeinfo for...` - - return { - software: { - name: "dotino-veloce", - version: { - major: 0, - minor: 1, - patch: 0, +export class NodeInfoComponent extends DvComponent { + handle(_ctx: Context): NodeInfo { + l.debug`Creating nodeinfo...` + return { + software: { + name: "dotino-veloce", + version: { + major: 0, + minor: 1, + patch: 0, + }, + repository: new URL("https://forge.steffo.eu/steffo/dotino-veloce"), }, - repository: new URL("https://forge.steffo.eu/steffo/dotino-veloce"), - }, - protocols: ["activitypub"], - usage: { - users: { - total: 0, - activeHalfyear: 0, - activeMonth: 0, - }, - localPosts: 0, - localComments: 0, + protocols: ["activitypub"], + usage: { + users: { + total: 0, + activeHalfyear: 0, + activeMonth: 0, + }, + localPosts: 0, + localComments: 0, + } } } -} \ No newline at end of file +} diff --git a/src/dv/undo.ts b/src/dv/undo.ts new file mode 100644 index 0000000..3baa390 --- /dev/null +++ b/src/dv/undo.ts @@ -0,0 +1,14 @@ +import { getLogger } from "@logtape/logtape" +import { ContextData } from "./index.ts" +import { Context, Undo } from "@fedify/fedify" +import { DvComponent } from "./component.ts" + + +const l = getLogger(["dotino-veloce", "dv", "undo"]) + + +export class UndoComponent extends DvComponent { + async handle(_ctx: Context, undo: Undo) { + l.error`Not implemented: ${undo}` + } +}