Compare commits
No commits in common. "292d3d3e7d7c5c93ba596e07c9d139395d0c25d1" and "325a8ad9adaedc229a9188d0d679693934f8d500" have entirely different histories.
292d3d3e7d
...
325a8ad9ad
15 changed files with 312 additions and 570 deletions
|
@ -1,18 +0,0 @@
|
||||||
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!`
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
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"])
|
|
||||||
|
|
||||||
export function createRouter(ap: DotinoVeloce) {
|
|
||||||
return async function router(request: Request, _info: Deno.ServeHandlerInfo): Promise<Response> {
|
|
||||||
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 === "/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 await handleHostMeta(url.origin)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
l.debug`Delegating request to federation handlers...`
|
|
||||||
return await ap.federation.fetch(
|
|
||||||
request,
|
|
||||||
{
|
|
||||||
contextData: undefined,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,249 +0,0 @@
|
||||||
// deno-lint-ignore-file require-await
|
|
||||||
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"
|
|
||||||
|
|
||||||
|
|
||||||
const l = getLogger(["dotino-veloce", "ap", "federation"])
|
|
||||||
|
|
||||||
|
|
||||||
type ContextData = undefined
|
|
||||||
|
|
||||||
|
|
||||||
export class DotinoVeloce {
|
|
||||||
stratz: StratzAPI
|
|
||||||
federation: Federation<ContextData>
|
|
||||||
|
|
||||||
constructor(kv: KvStore, stratz: StratzAPI) {
|
|
||||||
this.stratz = stratz
|
|
||||||
|
|
||||||
this.federation = createFederation<ContextData>({ kv })
|
|
||||||
|
|
||||||
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<ContextData>, handle: string): Partial<Actor> {
|
|
||||||
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<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({
|
|
||||||
...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({
|
|
||||||
...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({
|
|
||||||
...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) {
|
|
||||||
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)
|
|
||||||
|
|
||||||
return actor
|
|
||||||
}
|
|
||||||
|
|
||||||
async #actorMapper(_ctx: Context<ContextData>, handle: string) {
|
|
||||||
return handle
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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}`
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { getLogger } from "@logtape/logtape"
|
|
||||||
|
|
||||||
|
|
||||||
const l = getLogger(["dotino-veloce", "ap", "favicon"])
|
|
||||||
|
|
||||||
|
|
||||||
export async function handleFavicon(): Promise<Response> {
|
|
||||||
l.info`Returning favicon...`
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
await Deno.readFile(".media/icon-128x128_round.png"),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "image/png",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { getLogger } from "@logtape/logtape"
|
|
||||||
|
|
||||||
|
|
||||||
const l = getLogger(["dotino-veloce", "ap", "hostMeta"])
|
|
||||||
|
|
||||||
|
|
||||||
// deno-lint-ignore require-await
|
|
||||||
export async function handleHostMeta(origin: string): Promise<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(
|
|
||||||
`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link type="application/xrd+xml" template="${origin}/.well-known/webfinger?resource={uri}" rel="lrdd" /></XRD>`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/xml",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
140
src/federation.ts
Normal file
140
src/federation.ts
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
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<void>({ 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: `<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>`,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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: `[<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.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
|
|
@ -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, {})
|
|
||||||
}
|
|
|
@ -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({})
|
|
||||||
}
|
|
99
src/graphql/stratz.ts
Normal file
99
src/graphql/stratz.ts
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
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
|
||||||
|
}
|
40
src/handler.ts
Normal file
40
src/handler.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
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(
|
||||||
|
`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link type="application/xrd+xml" template="${requestUrl.origin}/.well-known/webfinger?resource={uri}" rel="lrdd" /></XRD>`,
|
||||||
|
{
|
||||||
|
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.
|
41
src/main.ts
41
src/main.ts
|
@ -1,23 +1,24 @@
|
||||||
import { doServe } from "./deno/server.ts"
|
import { configure, getConsoleSink, getLogger } from "@logtape/logtape"
|
||||||
import { createRedis } from "./fedify/redis.ts"
|
import { proxyHandler } from "./handler.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" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
async function main() {
|
const l = getLogger(["dotino-veloce", "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...`
|
||||||
main()
|
Deno.serve(
|
||||||
|
{
|
||||||
|
port: 8080,
|
||||||
|
onListen: (_localAddr) => {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
proxyHandler,
|
||||||
|
)
|
||||||
|
|
11
src/redis.ts
Normal file
11
src/redis.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
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, {})
|
|
@ -1,143 +0,0 @@
|
||||||
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<any> {
|
|
||||||
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<QueryPlayer | null> {
|
|
||||||
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<QueryGuild | null> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { configure, getConsoleSink } from "@logtape/logtape"
|
import { configure, getConsoleSink } from "@logtape/logtape"
|
||||||
import { doQueryPlayer } from "../src/stratz/api.ts"
|
import { doQueryPlayer } from "../src/graphql/stratz.ts"
|
||||||
import { assert, assertEquals} from "@std/assert"
|
import { assert, assertEquals} from "@std/assert"
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue