Compare commits
4 commits
b29211c387
...
569c4fbf32
Author | SHA1 | Date | |
---|---|---|---|
569c4fbf32 | |||
0f68d3ad14 | |||
548cfe139b | |||
0df82ac931 |
22 changed files with 547 additions and 414 deletions
|
@ -1,6 +1,7 @@
|
||||||
import Postgres from "@@npm/postgres"
|
import Postgres from "@@npm/postgres"
|
||||||
import { getLogger } from "@logtape/logtape"
|
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"
|
import { PostgresKvStore } from "@fedify/postgres/kv"
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,16 +16,6 @@ export type DbActor = {
|
||||||
private_ed: 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 {
|
export class Database {
|
||||||
sql: Postgres.Sql
|
sql: Postgres.Sql
|
||||||
|
@ -50,6 +41,11 @@ export class Database {
|
||||||
return new PostgresKvStore(this.sql, {})
|
return new PostgresKvStore(this.sql, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useAsMessageQueue(): MessageQueue {
|
||||||
|
l.info`Creating Postgres message queue...`
|
||||||
|
return new PostgresMessageQueue(this.sql, {})
|
||||||
|
}
|
||||||
|
|
||||||
async #doQueryFile<T>(path: string, variables: Postgres.ParameterOrJSON<any>[] = []): Promise<Postgres.RowList<T[]>> {
|
async #doQueryFile<T>(path: string, variables: Postgres.ParameterOrJSON<any>[] = []): Promise<Postgres.RowList<T[]>> {
|
||||||
try {
|
try {
|
||||||
l.debug`Running query at ${path}...`
|
l.debug`Running query at ${path}...`
|
||||||
|
@ -64,7 +60,7 @@ export class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
async reinitializeDev() {
|
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...`
|
l.info`01/XX Dropping public schema from database...`
|
||||||
await this.#doQueryFile("src/database/init/01-drop-schema.sql")
|
await this.#doQueryFile("src/database/init/01-drop-schema.sql")
|
||||||
|
@ -72,22 +68,10 @@ export class Database {
|
||||||
l.info`02/XX Recreating public schema...`
|
l.info`02/XX Recreating public schema...`
|
||||||
await this.#doQueryFile("src/database/init/02-create-schema.sql")
|
await this.#doQueryFile("src/database/init/02-create-schema.sql")
|
||||||
|
|
||||||
l.info`03/XX Creating applied migrations table...`
|
l.info`03/XX Creating actor table...`
|
||||||
await this.#doQueryFile("src/database/init/03-create-migration_applied.sql")
|
await this.#doQueryFile("src/database/init/03-create-actor.sql")
|
||||||
|
|
||||||
l.info`04/XX Creating actor table...`
|
l.info`04/XX Creating service actor...`
|
||||||
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...`
|
l.debug`Creating RSA key pair...`
|
||||||
const {publicKey: publicRsa, privateKey: privateRsa} = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5")
|
const {publicKey: publicRsa, privateKey: privateRsa} = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5")
|
||||||
l.debug`Creating elliptic curve key pair...`
|
l.debug`Creating elliptic curve key pair...`
|
||||||
|
@ -98,8 +82,8 @@ export class Database {
|
||||||
const privateRsaJwk = await exportJwk(privateRsa)
|
const privateRsaJwk = await exportJwk(privateRsa)
|
||||||
const privateEdJwk = await exportJwk(privateEd)
|
const privateEdJwk = await exportJwk(privateEd)
|
||||||
l.debug`Running SQL query...`
|
l.debug`Running SQL query...`
|
||||||
await this.#doQueryFile<DbActorService>(
|
await this.#doQueryFile<DbActor>(
|
||||||
"src/database/init/08-insert-actor_service.sql",
|
"src/database/init/04-insert-actor-app.sql",
|
||||||
[
|
[
|
||||||
publicRsaJwk,
|
publicRsaJwk,
|
||||||
publicEdJwk,
|
publicEdJwk,
|
||||||
|
@ -108,24 +92,24 @@ export class Database {
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
l.info`09/XX Creating follow table...`
|
l.info`05/XX Creating follow table...`
|
||||||
await this.#doQueryFile("src/database/init/09-create-follow.sql")
|
await this.#doQueryFile("src/database/init/05-create-follow.sql")
|
||||||
|
|
||||||
l.info("Done!")
|
l.info("Done!")
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActorService(): Promise<DbActorService> {
|
async getActorApp(): Promise<DbActor> {
|
||||||
l.info`Getting service actor entity...`
|
l.info`Getting app actor entity...`
|
||||||
|
|
||||||
const results = await this.#doQueryFile<DbActorService>("src/database/query/get-actor_service.sql")
|
const results = await this.#doQueryFile<DbActor>("src/database/query/get-actor_app.sql")
|
||||||
|
|
||||||
if(results.count == 0) {
|
if(results.count == 0) {
|
||||||
l.error`No service actor entity exists.`
|
l.error`No app actor entity exists.`
|
||||||
throw new Error("No service actor entity exists.")
|
throw new Error("No app actor entity exists.")
|
||||||
}
|
}
|
||||||
else if(results.count > 1) {
|
else if(results.count > 1) {
|
||||||
l.error`Multiple service actor entities exist.`
|
l.error`Multiple app actor entities exist.`
|
||||||
throw new Error("Multiple service actor entities exist.")
|
throw new Error("Multiple app actor entities exist.")
|
||||||
}
|
}
|
||||||
|
|
||||||
l.debug`Attempting to get only result...`
|
l.debug`Attempting to get only result...`
|
||||||
|
@ -135,10 +119,10 @@ export class Database {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActorPlayer(steamId: number): Promise<DbActorPlayer | null> {
|
async getActorPlayer(steamId: number): Promise<DbActor | null> {
|
||||||
l.info`Getting player actor entity with id: ${steamId}`
|
l.info`Getting player actor entity with id: ${steamId}`
|
||||||
|
|
||||||
const results = await this.#doQueryFile<DbActorPlayer>("src/database/query/get-actor_player.sql", [steamId])
|
const results = await this.#doQueryFile<DbActor>("src/database/query/get-actor_player.sql", [steamId])
|
||||||
|
|
||||||
if(results.count == 0) {
|
if(results.count == 0) {
|
||||||
l.debug`No such player actor entity exists.`
|
l.debug`No such player actor entity exists.`
|
||||||
|
@ -156,10 +140,10 @@ export class Database {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActorGuild(guildId: number): Promise<DbActorGuild | null> {
|
async getActorGuild(guildId: number): Promise<DbActor | null> {
|
||||||
l.info`Getting guild actor entity with id: ${guildId}`
|
l.info`Getting guild actor entity with id: ${guildId}`
|
||||||
|
|
||||||
const results = await this.#doQueryFile<DbActorGuild>("src/database/query/get-actor_guild.sql", [guildId])
|
const results = await this.#doQueryFile<DbActor>("src/database/query/get-actor_guild.sql", [guildId])
|
||||||
|
|
||||||
if(results.count == 0) {
|
if(results.count == 0) {
|
||||||
l.debug`No such guild actor entity exists.`
|
l.debug`No such guild actor entity exists.`
|
||||||
|
@ -182,4 +166,10 @@ export class Database {
|
||||||
|
|
||||||
await this.#doQueryFile<void>("src/database/query/put-follow.sql", [follower, followed])
|
await this.#doQueryFile<void>("src/database/query/put-follow.sql", [follower, followed])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteFollow(follower: string, followed: string): Promise<void> {
|
||||||
|
l.info`Deleting follow: from ${follower} to ${followed}`
|
||||||
|
|
||||||
|
await this.#doQueryFile<void>("src/database/query/delete-follow.sql", [follower, followed])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
8
src/database/init/03-create-actor.sql
Normal file
8
src/database/init/03-create-actor.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE TABLE actor (
|
||||||
|
handle VARCHAR PRIMARY KEY,
|
||||||
|
|
||||||
|
public_rsa JSON,
|
||||||
|
public_ed JSON,
|
||||||
|
private_rsa JSON,
|
||||||
|
private_ed JSON
|
||||||
|
);
|
|
@ -1,3 +0,0 @@
|
||||||
CREATE TABLE migration_applied (
|
|
||||||
code CHAR(4) PRIMARY KEY
|
|
||||||
);
|
|
|
@ -1,3 +0,0 @@
|
||||||
CREATE TABLE actor (
|
|
||||||
handle VARCHAR PRIMARY KEY
|
|
||||||
);
|
|
|
@ -1,11 +1,11 @@
|
||||||
INSERT INTO actor_service (
|
INSERT INTO actor (
|
||||||
handle,
|
handle,
|
||||||
public_rsa,
|
public_rsa,
|
||||||
public_ed,
|
public_ed,
|
||||||
private_rsa,
|
private_rsa,
|
||||||
private_ed
|
private_ed
|
||||||
) VALUES (
|
) VALUES (
|
||||||
'service',
|
'app',
|
||||||
$1,
|
$1,
|
||||||
$2,
|
$2,
|
||||||
$3,
|
$3,
|
|
@ -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
|
|
||||||
);
|
|
9
src/database/init/05-create-follow.sql
Normal file
9
src/database/init/05-create-follow.sql
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
CREATE TABLE follow (
|
||||||
|
follower VARCHAR NOT NULL,
|
||||||
|
followed VARCHAR REFERENCES actor (handle) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (
|
||||||
|
follower,
|
||||||
|
followed
|
||||||
|
)
|
||||||
|
);
|
|
@ -1,5 +0,0 @@
|
||||||
CREATE TABLE actor_player (
|
|
||||||
steam_id BIGINT UNIQUE NOT NULL
|
|
||||||
) INHERITS (
|
|
||||||
actor
|
|
||||||
);
|
|
|
@ -1,5 +0,0 @@
|
||||||
CREATE TABLE actor_guild (
|
|
||||||
guild_id INT UNIQUE NOT NULL
|
|
||||||
) INHERITS (
|
|
||||||
actor
|
|
||||||
);
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
);
|
|
|
@ -3,4 +3,4 @@ DELETE FROM
|
||||||
WHERE
|
WHERE
|
||||||
follow.follower = $1
|
follow.follower = $1
|
||||||
AND
|
AND
|
||||||
follow.followed = $2;
|
follow.followed = $2;
|
||||||
|
|
10
src/database/query/get-actor_app.sql
Normal file
10
src/database/query/get-actor_app.sql
Normal file
|
@ -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';
|
|
@ -1,12 +1,10 @@
|
||||||
SELECT
|
SELECT
|
||||||
actor_guild.handle,
|
actor.handle,
|
||||||
actor_guild.guild_id,
|
actor.public_rsa,
|
||||||
actor_service.public_rsa,
|
actor.public_ed,
|
||||||
actor_service.public_ed,
|
actor.private_rsa,
|
||||||
actor_service.private_rsa,
|
actor.private_ed
|
||||||
actor_service.private_ed
|
|
||||||
FROM
|
FROM
|
||||||
actor_guild,
|
actor
|
||||||
actor_service
|
|
||||||
WHERE
|
WHERE
|
||||||
actor_guild.steam_id = $1;
|
actor.handle = 'g' || $1;
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
SELECT
|
SELECT
|
||||||
actor_player.handle,
|
actor.handle,
|
||||||
actor_player.steam_id,
|
actor.public_rsa,
|
||||||
actor_service.public_rsa,
|
actor.public_ed,
|
||||||
actor_service.public_ed,
|
actor.private_rsa,
|
||||||
actor_service.private_rsa,
|
actor.private_ed
|
||||||
actor_service.private_ed
|
|
||||||
FROM
|
FROM
|
||||||
actor_player,
|
actor
|
||||||
actor_service
|
|
||||||
WHERE
|
WHERE
|
||||||
actor_player.steam_id = $1;
|
actor.handle = 'u' || $1;
|
||||||
|
|
|
@ -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;
|
|
196
src/dv/actor.ts
Normal file
196
src/dv/actor.ts
Normal file
|
@ -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<ContextData>, handle: string): Promise<Actor | null> {
|
||||||
|
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<ContextData>, handle: string): Promise<Partial<Actor>> {
|
||||||
|
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<ContextData>, handle: string): Promise<Actor | null> {
|
||||||
|
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: `<a href="https://forge.steffo.eu/steffo/dotino-veloce">https://forge.steffo.eu/steffo/dotino-veloce</a>`,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
lApp.debug`Generated ActivityPub actor for service: ${actor}`
|
||||||
|
return actor
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPlayerActor(ctx: Context<ContextData>, handle: string): Promise<Actor | null> {
|
||||||
|
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: `<a href="https://steamcommunity.com/profiles/[U:1:${player.id}]">https://steamcommunity.com/profiles/[U:1:${player.id}]</a>`,
|
||||||
|
}),
|
||||||
|
new PropertyValue({
|
||||||
|
name: "STRATZ",
|
||||||
|
value: `<a href="https://stratz.com/players/${player.id}">https://stratz.com/players/${player.id}</a>`,
|
||||||
|
}),
|
||||||
|
new PropertyValue({
|
||||||
|
name: "OpenDota",
|
||||||
|
value: `<a href="https://www.opendota.com/players/${player.id}">https://www.opendota.com/players/${player.id}</a>`,
|
||||||
|
}),
|
||||||
|
new PropertyValue({
|
||||||
|
name: "DOTABUFF",
|
||||||
|
value: `<a href="https://www.dotabuff.com/players/${player.id}">https://www.dotabuff.com/players/${player.id}</a>`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
lPlayer.debug`Generated ActivityPub actor for player ${steamId}: ${actor}`
|
||||||
|
|
||||||
|
return actor
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGuildActor(ctx: Context<ContextData>, handle: string): Promise<Actor | null> {
|
||||||
|
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: `[<b>${escapeHtml(guild.tag)}</b>]`,
|
||||||
|
}),
|
||||||
|
new PropertyValue({
|
||||||
|
name: "Message of the day",
|
||||||
|
value: escapeHtml(guild.motd),
|
||||||
|
}),
|
||||||
|
new PropertyValue({
|
||||||
|
name: "STRATZ",
|
||||||
|
value: `<a href="https://stratz.com/guilds/${guild.id}">https://stratz.com/guilds/${guild.id}</a>`,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
lGuild.debug`Generated ActivityPub actor for guild ${guildId}: ${actor}`
|
||||||
|
|
||||||
|
return actor
|
||||||
|
}
|
||||||
|
}
|
15
src/dv/component.ts
Normal file
15
src/dv/component.ts
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
114
src/dv/follow.ts
Normal file
114
src/dv/follow.ts
Normal file
|
@ -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<ContextData>, 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<ContextData>, 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!`
|
||||||
|
}
|
||||||
|
}
|
339
src/dv/index.ts
339
src/dv/index.ts
|
@ -1,15 +1,18 @@
|
||||||
// deno-lint-ignore-file require-await
|
import { createFederation, Federation, Follow, Undo } 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 { 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 { StratzAPI } from "../stratz/api.ts"
|
||||||
import { Database } from "../database/index.ts"
|
import { Database } from "../database/index.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"])
|
const l = getLogger(["dotino-veloce", "dv", "index"])
|
||||||
|
|
||||||
|
|
||||||
type ContextData = undefined
|
export type ContextData = undefined
|
||||||
|
|
||||||
|
|
||||||
export class DotinoVeloce {
|
export class DotinoVeloce {
|
||||||
|
@ -17,327 +20,43 @@ export class DotinoVeloce {
|
||||||
stratz: StratzAPI
|
stratz: StratzAPI
|
||||||
federation: Federation<ContextData>
|
federation: Federation<ContextData>
|
||||||
|
|
||||||
|
cNodeInfo: NodeInfoComponent
|
||||||
|
cActor: ActorComponent
|
||||||
|
cKey: KeyComponent
|
||||||
|
cFollow: FollowComponent
|
||||||
|
cUndo: UndoComponent
|
||||||
|
|
||||||
constructor(db: Database, stratz: StratzAPI) {
|
constructor(db: Database, stratz: StratzAPI) {
|
||||||
this.db = db
|
this.db = db
|
||||||
this.stratz = stratz
|
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<ContextData>({
|
this.federation = createFederation<ContextData>({
|
||||||
kv: db.useAsKvStore(),
|
kv: db.useAsKvStore(),
|
||||||
|
queue: db.useAsMessageQueue(),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.federation
|
this.federation
|
||||||
.setActorDispatcher("/users/{identifier}", this.#actorHandler.bind(this))
|
.setActorDispatcher("/users/{identifier}", this.cActor.getActor.bind(this.cActor))
|
||||||
.setKeyPairsDispatcher(this.#actorKeys.bind(this))
|
.setKeyPairsDispatcher(this.cKey.getActorKeys.bind(this.cKey))
|
||||||
.mapHandle(this.#actorMapper.bind(this))
|
.mapHandle((_, h) => h)
|
||||||
|
|
||||||
this.federation
|
this.federation
|
||||||
.setInboxListeners("/inbox/{identifier}", "/inbox")
|
.setInboxListeners("/inbox/{identifier}", "/inbox")
|
||||||
// Akkoma with Authorized Fetch requires this to be set
|
// Akkoma with Authorized Fetch requires this to be set
|
||||||
// https://p.junimo.party/#/junimo.party/s/AnFW6s3OURPMY04LKq?view=full
|
// https://p.junimo.party/#/junimo.party/s/AnFW6s3OURPMY04LKq?view=full
|
||||||
.setSharedKeyDispatcher(this.#instanceKeys.bind(this))
|
.setSharedKeyDispatcher(this.cKey.getFetchKey.bind(this.cKey))
|
||||||
.on(Follow, this.#followHandler.bind(this))
|
.on(Follow, this.cFollow.handlePlus.bind(this.cFollow))
|
||||||
|
.on(Undo, this.cUndo.handle.bind(this.cUndo))
|
||||||
|
|
||||||
this.federation
|
this.federation
|
||||||
.setNodeInfoDispatcher("/nodeinfo/2.1", this.#nodeInfoHandler.bind(this))
|
.setNodeInfoDispatcher("/nodeinfo/2.1", this.cNodeInfo.handle.bind(this.cNodeInfo))
|
||||||
|
|
||||||
// TODO: Setup a message queue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async #nodeInfoHandler(_ctx: Context<ContextData>): Promise<NodeInfo> {
|
// REMINDER: A pattern matcher on received activities could be useful, actually.
|
||||||
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<ContextData>, handle: string): Promise<Partial<Actor>> {
|
|
||||||
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<ContextData>, handle: string): Promise<Actor | null> {
|
|
||||||
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: `<a href="https://steamcommunity.com/profiles/[U:1:${player.id}]">https://steamcommunity.com/profiles/[U:1:${player.id}]</a>`,
|
|
||||||
}),
|
|
||||||
new PropertyValue({
|
|
||||||
name: "STRATZ",
|
|
||||||
value: `<a href="https://stratz.com/players/${player.id}">https://stratz.com/players/${player.id}</a>`,
|
|
||||||
}),
|
|
||||||
new PropertyValue({
|
|
||||||
name: "OpenDota",
|
|
||||||
value: `<a href="https://www.opendota.com/players/${player.id}">https://www.opendota.com/players/${player.id}</a>`,
|
|
||||||
}),
|
|
||||||
new PropertyValue({
|
|
||||||
name: "DOTABUFF",
|
|
||||||
value: `<a href="https://www.dotabuff.com/players/${player.id}">https://www.dotabuff.com/players/${player.id}</a>`,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
l.debug`Generated ActivityPub actor for player ${steamId}: ${actor}`
|
|
||||||
|
|
||||||
return actor
|
|
||||||
}
|
|
||||||
|
|
||||||
static guildMatcher = /g([0-9]+)/
|
|
||||||
|
|
||||||
async #guildActor(ctx: Context<ContextData>, handle: string): Promise<Actor | null> {
|
|
||||||
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: `[<b>${escapeHtml(guild.tag)}</b>]`,
|
|
||||||
}),
|
|
||||||
new PropertyValue({
|
|
||||||
name: "Message of the day",
|
|
||||||
value: escapeHtml(guild.motd),
|
|
||||||
}),
|
|
||||||
new PropertyValue({
|
|
||||||
name: "STRATZ",
|
|
||||||
value: `<a href="https://stratz.com/guilds/${guild.id}">https://stratz.com/guilds/${guild.id}</a>`,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
l.debug`Generated ActivityPub actor for guild ${guildId}: ${actor}`
|
|
||||||
|
|
||||||
return actor
|
|
||||||
}
|
|
||||||
|
|
||||||
static serviceMatcher = /service/
|
|
||||||
|
|
||||||
async #serviceActor(ctx: Context<ContextData>, handle: string): Promise<Actor | null> {
|
|
||||||
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: `<a href="https://forge.steffo.eu/steffo/dotino-veloce">https://forge.steffo.eu/steffo/dotino-veloce</a>`,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
l.debug`Generated ActivityPub actor for service: ${actor}`
|
|
||||||
return actor
|
|
||||||
}
|
|
||||||
|
|
||||||
async #actorHandler(ctx: Context<ContextData>, handle: string): Promise<Actor | null> {
|
|
||||||
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<ContextData>, handle: string): Promise<string> {
|
|
||||||
l.debug`Getting handle mapping for: ${handle}`
|
|
||||||
return handle
|
|
||||||
}
|
|
||||||
|
|
||||||
async #actorKeys(_ctx: Context<ContextData>, handle: string): Promise<CryptoKeyPair[]> {
|
|
||||||
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<ContextData>): Promise<{ identifier: string }> {
|
|
||||||
l.debug`Getting instance keys...`
|
|
||||||
return {"identifier": "garasauto"}
|
|
||||||
}
|
|
||||||
|
|
||||||
async #authorizationHandler(_ctx: Context<ContextData>, _handle: string, _signedKey: unknown, _signedKeyOwner: unknown): Promise<boolean> {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
async #followHandler(ctx: Context<ContextData>, 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,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
68
src/dv/key.ts
Normal file
68
src/dv/key.ts
Normal file
|
@ -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<ContextData>, handle: string): Promise<CryptoKeyPair[]> {
|
||||||
|
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<ContextData>): Promise<SenderKeyPair> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
35
src/dv/nodeinfo.ts
Normal file
35
src/dv/nodeinfo.ts
Normal file
|
@ -0,0 +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 class NodeInfoComponent extends DvComponent {
|
||||||
|
handle(_ctx: Context<ContextData>): 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"),
|
||||||
|
},
|
||||||
|
protocols: ["activitypub"],
|
||||||
|
usage: {
|
||||||
|
users: {
|
||||||
|
total: 0,
|
||||||
|
activeHalfyear: 0,
|
||||||
|
activeMonth: 0,
|
||||||
|
},
|
||||||
|
localPosts: 0,
|
||||||
|
localComments: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/dv/undo.ts
Normal file
14
src/dv/undo.ts
Normal file
|
@ -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<ContextData>, undo: Undo) {
|
||||||
|
l.error`Not implemented: ${undo}`
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue