Some progress towards accepting and rejecting follows #3

Merged
steffo merged 22 commits from feature/follows into main 2024-10-23 05:47:18 +00:00
15 changed files with 315 additions and 133 deletions
Showing only changes of commit 92d543dbbd - Show all commits

18
.vscode/launch.json vendored
View file

@ -6,9 +6,9 @@
"configurations": [ "configurations": [
{ {
"request": "launch", "request": "launch",
"name": "Dev Server", "name": "Reinitialize database",
"type": "node", "type": "node",
"program": "${workspaceFolder}/main.ts", "program": "${workspaceFolder}/src/entry/dbInit.ts",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/local.env", "envFile": "${workspaceFolder}/local.env",
"runtimeExecutable": "/usr/bin/deno", "runtimeExecutable": "/usr/bin/deno",
@ -17,28 +17,26 @@
"--watch", "--watch",
"--no-prompt", "--no-prompt",
"--allow-all", "--allow-all",
"--inspect-wait", "--inspect-wait"
"src/main.ts"
], ],
"attachSimplePort": 9229 "attachSimplePort": 9229
}, },
{ {
"request": "launch", "request": "launch",
"name": "Init database", "name": "Dev Server",
"type": "node", "type": "node",
"program": "${workspaceFolder}/main.ts", "program": "${workspaceFolder}/src/entry/server.ts",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/local.env", "envFile": "${workspaceFolder}/local.env",
"runtimeExecutable": "/usr/bin/deno", "runtimeExecutable": "/usr/bin/deno",
"runtimeArgs": [ "runtimeArgs": [
"run", "run",
"--watch",
"--no-prompt", "--no-prompt",
"--allow-all", "--allow-all",
"--inspect-wait", "--inspect-wait"
"src/database/init/index.ts"
], ],
"attachSimplePort": 9229 "attachSimplePort": 9229
}, }
] ]
} }

176
src/database/index.ts Normal file
View file

@ -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<T>(path: string, variables: Postgres.ParameterOrJSON<any>[] = []): Promise<Postgres.RowList<T[]>> {
try {
l.debug`Running query at ${path}...`
var results = await this.sql.file<T[]>(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<DbActorService>(
"src/database/init/08-insert-actor_service.sql",
[
publicRsaJwk,
publicEdJwk,
privateRsaJwk,
privateEdJwk,
]
)
l.info("Done!")
}
async getActorService(): Promise<DbActorService> {
l.info`Getting service actor entity...`
const results = await this.#doQueryFile<DbActorService>("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<DbActorPlayer | null> {
l.info`Getting player actor entity with id: ${steamId}`
const results = await this.#doQueryFile<DbActorPlayer>("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<DbActorGuild | null> {
l.info`Getting guild actor entity with id: ${guildId}`
const results = await this.#doQueryFile<DbActorGuild>("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
}
}

View file

@ -1,9 +1,3 @@
CREATE TABLE actor ( CREATE TABLE actor (
handle VARCHAR PRIMARY KEY, handle VARCHAR PRIMARY KEY
public_rsassa BYTEA NOT NULL,
private_rsassa BYTEA NOT NULL,
public_jwk BYTEA NOT NULL,
private_jwk BYTEA NOT NULL
); );

View file

@ -1,5 +1,8 @@
CREATE TABLE actor_service ( 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 ( ) INHERITS (
actor actor
); );

View file

@ -0,0 +1,13 @@
INSERT INTO actor_service (
handle,
public_rsa,
public_ed,
private_rsa,
private_ed
) VALUES (
'service',
$1,
$2,
$3,
$4
);

View file

@ -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()

View file

@ -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, {})
}

View file

@ -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)
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -1,8 +1,9 @@
// deno-lint-ignore-file require-await // 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 { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts"
import { escapeHtml } from "@@x/escape" import { escapeHtml } from "@@x/escape"
import { StratzAPI } from "../stratz/api.ts" import { StratzAPI } from "../stratz/api.ts"
import { Database } from "../database/index.ts"
const l = getLogger(["dotino-veloce", "ap", "federation"]) const l = getLogger(["dotino-veloce", "ap", "federation"])
@ -12,39 +13,39 @@ type ContextData = undefined
export class DotinoVeloce { export class DotinoVeloce {
db: Database
stratz: StratzAPI stratz: StratzAPI
federation: Federation<ContextData> federation: Federation<ContextData>
constructor(kv: KvStore, stratz: StratzAPI) { constructor(db: Database, stratz: StratzAPI) {
this.db = db
this.stratz = stratz this.stratz = stratz
this.federation = createFederation<ContextData>({ kv }) this.federation = createFederation<ContextData>({ kv: db.useAsKvStore() })
this.federation this.federation
.setActorDispatcher("/users/{identifier}", this.#actorHandler.bind(this)) .setActorDispatcher("/users/{identifier}", this.#actorHandler.bind(this))
.setKeyPairsDispatcher(this.#actorKeys.bind(this))
.mapHandle(this.#actorMapper.bind(this)) .mapHandle(this.#actorMapper.bind(this))
// .setKeyPairsDispatcher(this.#actorKeys.bind(this))
this.federation this.federation
.setInboxListeners("/inbox/{identifier}", "/inbox") .setInboxListeners("/inbox/{identifier}", "/inbox")
.on(Follow, this.#followHandler.bind(this)) .on(Follow, this.#followHandler.bind(this))
} }
#commonActorProperties(ctx: Context<ContextData>, handle: string): Partial<Actor> { async #commonActorProperties(ctx: Context<ContextData>, handle: string): Promise<Partial<Actor>> {
l.debug`Generating common actor properties for ${handle}...` l.debug`Generating common actor properties for ${handle}...`
const properties = { const properties = {
preferredUsername: handle,
id: ctx.getActorUri(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), inbox: ctx.getInboxUri(handle),
endpoints: new Endpoints({ endpoints: new Endpoints({
sharedInbox: ctx.getInboxUri(), 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}` l.debug`Generated common actor properties for ${handle}: ${properties}`
return properties return properties
@ -79,7 +80,7 @@ export class DotinoVeloce {
l.debug`Generating ActivityPub actor for player ${steamId}...` l.debug`Generating ActivityPub actor for player ${steamId}...`
const actor = new Person({ const actor = new Person({
...this.#commonActorProperties(ctx, handle), ...(await this.#commonActorProperties(ctx, handle)),
name: `[TEST] ${escapeHtml(player.name)}`, name: `[TEST] ${escapeHtml(player.name)}`,
icon: new Image({ icon: new Image({
@ -140,7 +141,7 @@ export class DotinoVeloce {
l.debug`Generating ActivityPub actor for guild ${guildId}...` l.debug`Generating ActivityPub actor for guild ${guildId}...`
const actor = new Organization({ const actor = new Organization({
...this.#commonActorProperties(ctx, handle), ...(await this.#commonActorProperties(ctx, handle)),
name: `[TEST] ${escapeHtml(guild.name)}`, name: `[TEST] ${escapeHtml(guild.name)}`,
summary: escapeHtml(guild.description), summary: escapeHtml(guild.description),
@ -181,7 +182,7 @@ export class DotinoVeloce {
l.debug`Generating ActivityPub actor for service...` l.debug`Generating ActivityPub actor for service...`
const actor = new Application({ const actor = new Application({
...this.#commonActorProperties(ctx, handle), ...(await this.#commonActorProperties(ctx, handle)),
name: "[TEST] Dotino Veloce", name: "[TEST] Dotino Veloce",
summary: "Service account of a Dotino Veloce instance.", summary: "Service account of a Dotino Veloce instance.",
@ -210,11 +211,41 @@ export class DotinoVeloce {
} }
async #actorMapper(_ctx: Context<ContextData>, handle: string): Promise<string> { async #actorMapper(_ctx: Context<ContextData>, handle: string): Promise<string> {
l.debug`Getting handle mapping for: ${handle}`
return handle return handle
} }
async #actorKeys(ctx: Context<ContextData>, handle: string): Promise<CryptoKeyPair[]> { async #actorKeys(_ctx: Context<ContextData>, handle: string): Promise<CryptoKeyPair[]> {
throw "TODO: Not implemented" 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<ContextData>, follow: Follow) { async #followHandler(ctx: Context<ContextData>, follow: Follow) {

12
src/entry/dbInit.ts Normal file
View file

@ -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()

21
src/entry/server.ts Normal file
View file

@ -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()

View file

@ -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()