Some progress towards accepting and rejecting follows #3
35 changed files with 1227 additions and 324 deletions
40
.vscode/launch.json
vendored
Normal file
40
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Reinitialize database",
|
||||||
|
"type": "node",
|
||||||
|
"runtimeExecutable": "/usr/bin/deno",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"--no-prompt",
|
||||||
|
"--allow-all",
|
||||||
|
"--inspect-wait"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}/src/entry/dbInit.ts",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"envFile": "${workspaceFolder}/local.env",
|
||||||
|
"attachSimplePort": 9229
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Dev Server",
|
||||||
|
"type": "node",
|
||||||
|
"runtimeExecutable": "/usr/bin/deno",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"--no-prompt",
|
||||||
|
"--allow-all",
|
||||||
|
"--inspect-wait"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}/src/entry/server.ts",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"envFile": "${workspaceFolder}/local.env",
|
||||||
|
"attachSimplePort": 9229
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,13 +1,14 @@
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
"@@npm/ioredis": "npm:ioredis@^5.4.1",
|
"@@npm/ioredis": "npm:ioredis@^5.4.1",
|
||||||
|
"@@x/escape": "https://deno.land/x/escape@1.3.0/mod.ts",
|
||||||
"@fedify/fedify": "jsr:@fedify/fedify@^1.0.2",
|
"@fedify/fedify": "jsr:@fedify/fedify@^1.0.2",
|
||||||
"@fedify/redis": "jsr:@fedify/redis@^0.3.0",
|
"@fedify/postgres": "jsr:@fedify/postgres@^0.1.0",
|
||||||
"@hongminhee/x-forwarded-fetch": "jsr:@hongminhee/x-forwarded-fetch@^0.2.0",
|
"@hongminhee/x-forwarded-fetch": "jsr:@hongminhee/x-forwarded-fetch@^0.2.0",
|
||||||
"@logtape/logtape": "jsr:@logtape/logtape@^0.6.3",
|
"@logtape/logtape": "jsr:@logtape/logtape@^0.6.3",
|
||||||
"@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0",
|
"@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0",
|
||||||
"@std/assert": "jsr:@std/assert@1",
|
"@std/assert": "jsr:@std/assert@1",
|
||||||
"@@x/escape": "https://deno.land/x/escape@1.3.0/mod.ts"
|
"@@npm/postgres": "npm:postgres@^3.4.4"
|
||||||
},
|
},
|
||||||
"unstable": [
|
"unstable": [
|
||||||
"temporal"
|
"temporal"
|
||||||
|
|
127
deno.lock
127
deno.lock
|
@ -4,7 +4,7 @@
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
"jsr:@fedify/fedify@^1.0.0": "jsr:@fedify/fedify@1.0.2",
|
"jsr:@fedify/fedify@^1.0.0": "jsr:@fedify/fedify@1.0.2",
|
||||||
"jsr:@fedify/fedify@^1.0.2": "jsr:@fedify/fedify@1.0.2",
|
"jsr:@fedify/fedify@^1.0.2": "jsr:@fedify/fedify@1.0.2",
|
||||||
"jsr:@fedify/redis@^0.3.0": "jsr:@fedify/redis@0.3.0",
|
"jsr:@fedify/postgres@^0.1.0": "jsr:@fedify/postgres@0.1.0",
|
||||||
"jsr:@hongminhee/x-forwarded-fetch@^0.2.0": "jsr:@hongminhee/x-forwarded-fetch@0.2.0",
|
"jsr:@hongminhee/x-forwarded-fetch@^0.2.0": "jsr:@hongminhee/x-forwarded-fetch@0.2.0",
|
||||||
"jsr:@hugoalh/http-header-link@^1.0.2": "jsr:@hugoalh/http-header-link@1.0.2",
|
"jsr:@hugoalh/http-header-link@^1.0.2": "jsr:@hugoalh/http-header-link@1.0.2",
|
||||||
"jsr:@hugoalh/is-string-singleline@1.0.2": "jsr:@hugoalh/is-string-singleline@1.0.2",
|
"jsr:@hugoalh/is-string-singleline@1.0.2": "jsr:@hugoalh/is-string-singleline@1.0.2",
|
||||||
|
@ -20,12 +20,14 @@
|
||||||
"npm:@phensley/language-tag@^1.9.0": "npm:@phensley/language-tag@1.9.0",
|
"npm:@phensley/language-tag@^1.9.0": "npm:@phensley/language-tag@1.9.0",
|
||||||
"npm:@types/node": "npm:@types/node@18.16.19",
|
"npm:@types/node": "npm:@types/node@18.16.19",
|
||||||
"npm:asn1js@^3.0.5": "npm:asn1js@3.0.5",
|
"npm:asn1js@^3.0.5": "npm:asn1js@3.0.5",
|
||||||
|
"npm:graffle@next": "npm:graffle@8.0.0-next.68_@opentelemetry+api@1.9.0_graphql@16.9.0",
|
||||||
"npm:ioredis@^5.4.1": "npm:ioredis@5.4.1",
|
"npm:ioredis@^5.4.1": "npm:ioredis@5.4.1",
|
||||||
"npm:json-canon@^1.0.1": "npm:json-canon@1.0.1",
|
"npm:json-canon@^1.0.1": "npm:json-canon@1.0.1",
|
||||||
"npm:jsonld@^8.3.2": "npm:jsonld@8.3.2",
|
"npm:jsonld@^8.3.2": "npm:jsonld@8.3.2",
|
||||||
"npm:multibase@^4.0.6": "npm:multibase@4.0.6",
|
"npm:multibase@^4.0.6": "npm:multibase@4.0.6",
|
||||||
"npm:multicodec@^3.2.1": "npm:multicodec@3.2.1",
|
"npm:multicodec@^3.2.1": "npm:multicodec@3.2.1",
|
||||||
"npm:pkijs@^3.2.4": "npm:pkijs@3.2.4",
|
"npm:pkijs@^3.2.4": "npm:pkijs@3.2.4",
|
||||||
|
"npm:postgres@^3.4.4": "npm:postgres@3.4.4",
|
||||||
"npm:uri-template-router@^0.0.16": "npm:uri-template-router@0.0.16",
|
"npm:uri-template-router@^0.0.16": "npm:uri-template-router@0.0.16",
|
||||||
"npm:url-template@^3.1.1": "npm:url-template@3.1.1"
|
"npm:url-template@^3.1.1": "npm:url-template@3.1.1"
|
||||||
},
|
},
|
||||||
|
@ -50,12 +52,11 @@
|
||||||
"npm:url-template@^3.1.1"
|
"npm:url-template@^3.1.1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@fedify/redis@0.3.0": {
|
"@fedify/postgres@0.1.0": {
|
||||||
"integrity": "48068af7ad24d4f6c6935d6f869659faeb92281a9e796921f9ba7d94e74f8cfc",
|
"integrity": "350e3e535372d84acebe7392ae98495e388f30d75165d52c6d32a8dc6e940b80",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@fedify/fedify@^1.0.0",
|
"jsr:@fedify/fedify@^1.0.0",
|
||||||
"jsr:@logtape/logtape@^0.6.3",
|
"npm:postgres@^3.4.4"
|
||||||
"npm:ioredis@^5.4.1"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@hongminhee/x-forwarded-fetch@0.2.0": {
|
"@hongminhee/x-forwarded-fetch@0.2.0": {
|
||||||
|
@ -108,10 +109,38 @@
|
||||||
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
|
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
|
"@graphql-typed-document-node/core@3.2.0_graphql@16.9.0": {
|
||||||
|
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"graphql": "graphql@16.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@ioredis/commands@1.2.0": {
|
"@ioredis/commands@1.2.0": {
|
||||||
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
|
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
|
"@molt/command@0.9.0": {
|
||||||
|
"integrity": "sha512-1JI8dAlpqlZoXyKWVQggX7geFNPxBpocHIXQCsnxDjKy+3WX4SGyZVJXuLlqRRrX7FmQCuuMAfx642ovXmPA9g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@molt/types": "@molt/types@0.2.0",
|
||||||
|
"alge": "alge@0.8.1",
|
||||||
|
"chalk": "chalk@5.3.0",
|
||||||
|
"lodash.camelcase": "lodash.camelcase@4.3.0",
|
||||||
|
"lodash.snakecase": "lodash.snakecase@4.1.1",
|
||||||
|
"readline-sync": "readline-sync@1.4.10",
|
||||||
|
"string-length": "string-length@6.0.0",
|
||||||
|
"strip-ansi": "strip-ansi@7.1.0",
|
||||||
|
"ts-toolbelt": "ts-toolbelt@9.6.0",
|
||||||
|
"type-fest": "type-fest@4.26.1",
|
||||||
|
"zod": "zod@3.23.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@molt/types@0.2.0": {
|
||||||
|
"integrity": "sha512-p6ChnEZDGjg9PYPec9BK6Yp5/DdSrYQvXTBAtgrnqX6N36cZy37ql1c8Tc5LclfIYBNG7EZp8NBcRTYJwyi84g==",
|
||||||
|
"dependencies": {
|
||||||
|
"ts-toolbelt": "ts-toolbelt@9.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@multiformats/base-x@4.0.1": {
|
"@multiformats/base-x@4.0.1": {
|
||||||
"integrity": "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==",
|
"integrity": "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
|
@ -140,6 +169,19 @@
|
||||||
"event-target-shim": "event-target-shim@5.0.1"
|
"event-target-shim": "event-target-shim@5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"alge@0.8.1": {
|
||||||
|
"integrity": "sha512-kiV9nTt+XIauAXsowVygDxMZLplZxDWt0W8plE/nB32/V2ziM/P/TxDbSVK7FYIUt2Xo16h3/htDh199LNPCKQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.ismatch": "lodash.ismatch@4.4.0",
|
||||||
|
"remeda": "remeda@1.61.0",
|
||||||
|
"ts-toolbelt": "ts-toolbelt@9.6.0",
|
||||||
|
"zod": "zod@3.23.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ansi-regex@6.1.0": {
|
||||||
|
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
"asn1js@3.0.5": {
|
"asn1js@3.0.5": {
|
||||||
"integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==",
|
"integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -156,6 +198,10 @@
|
||||||
"integrity": "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==",
|
"integrity": "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
|
"chalk@5.3.0": {
|
||||||
|
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
"cluster-key-slot@1.1.2": {
|
"cluster-key-slot@1.1.2": {
|
||||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
|
@ -191,6 +237,20 @@
|
||||||
"fetch-blob": "fetch-blob@3.2.0"
|
"fetch-blob": "fetch-blob@3.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"graffle@8.0.0-next.68_@opentelemetry+api@1.9.0_graphql@16.9.0": {
|
||||||
|
"integrity": "sha512-oaigY1yEX48cUSiFJKtigjGw4ooA7JtLwafSUQuuAgbl0qtnaSmAGrfSRpb9RhxF0y/HEzJNEMYUaea085cn8w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@graphql-typed-document-node/core": "@graphql-typed-document-node/core@3.2.0_graphql@16.9.0",
|
||||||
|
"@molt/command": "@molt/command@0.9.0",
|
||||||
|
"@opentelemetry/api": "@opentelemetry/api@1.9.0",
|
||||||
|
"graphql": "graphql@16.9.0",
|
||||||
|
"is-plain-obj": "is-plain-obj@4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graphql@16.9.0": {
|
||||||
|
"integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
"ioredis@5.4.1": {
|
"ioredis@5.4.1": {
|
||||||
"integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==",
|
"integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -205,6 +265,10 @@
|
||||||
"standard-as-callback": "standard-as-callback@2.1.0"
|
"standard-as-callback": "standard-as-callback@2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"is-plain-obj@4.1.0": {
|
||||||
|
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
"json-canon@1.0.1": {
|
"json-canon@1.0.1": {
|
||||||
"integrity": "sha512-PQcj4PFOTAQxE8PgoQ4KrM0DcKWZd7S3ELOON8rmysl9I8JuFMgxu1H9v+oZsTPjjkpeS3IHPwLjr7d+gKygnw==",
|
"integrity": "sha512-PQcj4PFOTAQxE8PgoQ4KrM0DcKWZd7S3ELOON8rmysl9I8JuFMgxu1H9v+oZsTPjjkpeS3IHPwLjr7d+gKygnw==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
|
@ -230,6 +294,10 @@
|
||||||
"integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==",
|
"integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
|
"lodash.camelcase@4.3.0": {
|
||||||
|
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
"lodash.defaults@4.2.0": {
|
"lodash.defaults@4.2.0": {
|
||||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
|
@ -238,6 +306,14 @@
|
||||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
|
"lodash.ismatch@4.4.0": {
|
||||||
|
"integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"lodash.snakecase@4.1.1": {
|
||||||
|
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
"lru-cache@6.0.0": {
|
"lru-cache@6.0.0": {
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -288,6 +364,10 @@
|
||||||
"tslib": "tslib@2.7.0"
|
"tslib": "tslib@2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"postgres@3.4.4": {
|
||||||
|
"integrity": "sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
"pvtsutils@1.3.5": {
|
"pvtsutils@1.3.5": {
|
||||||
"integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==",
|
"integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -304,6 +384,10 @@
|
||||||
"setimmediate": "setimmediate@1.0.5"
|
"setimmediate": "setimmediate@1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"readline-sync@1.4.10": {
|
||||||
|
"integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
"redis-errors@1.2.0": {
|
"redis-errors@1.2.0": {
|
||||||
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
|
@ -314,6 +398,10 @@
|
||||||
"redis-errors": "redis-errors@1.2.0"
|
"redis-errors": "redis-errors@1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"remeda@1.61.0": {
|
||||||
|
"integrity": "sha512-caKfSz9rDeSKBQQnlJnVW3mbVdFgxgGWQKq1XlFokqjf+hQD5gxutLGTTY2A/x24UxVyJe9gH5fAkFI63ULw4A==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
"setimmediate@1.0.5": {
|
"setimmediate@1.0.5": {
|
||||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
|
@ -322,10 +410,30 @@
|
||||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
|
"string-length@6.0.0": {
|
||||||
|
"integrity": "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==",
|
||||||
|
"dependencies": {
|
||||||
|
"strip-ansi": "strip-ansi@7.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strip-ansi@7.1.0": {
|
||||||
|
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "ansi-regex@6.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ts-toolbelt@9.6.0": {
|
||||||
|
"integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
"tslib@2.7.0": {
|
"tslib@2.7.0": {
|
||||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
|
"type-fest@4.26.1": {
|
||||||
|
"integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
"uint8arrays@3.1.1": {
|
"uint8arrays@3.1.1": {
|
||||||
"integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==",
|
"integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -357,6 +465,10 @@
|
||||||
"yallist@4.0.0": {
|
"yallist@4.0.0": {
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"zod@3.23.8": {
|
||||||
|
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||||
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -508,12 +620,13 @@
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@fedify/fedify@^1.0.2",
|
"jsr:@fedify/fedify@^1.0.2",
|
||||||
"jsr:@fedify/redis@^0.3.0",
|
"jsr:@fedify/postgres@^0.1.0",
|
||||||
"jsr:@hongminhee/x-forwarded-fetch@^0.2.0",
|
"jsr:@hongminhee/x-forwarded-fetch@^0.2.0",
|
||||||
"jsr:@logtape/logtape@^0.6.3",
|
"jsr:@logtape/logtape@^0.6.3",
|
||||||
"jsr:@std/assert@1",
|
"jsr:@std/assert@1",
|
||||||
"npm:@opentelemetry/api@^1.9.0",
|
"npm:@opentelemetry/api@^1.9.0",
|
||||||
"npm:ioredis@^5.4.1"
|
"npm:ioredis@^5.4.1",
|
||||||
|
"npm:postgres@^3.4.4"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
175
src/database/index.ts
Normal file
175
src/database/index.ts
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
import Postgres from "@@npm/postgres"
|
||||||
|
import { getLogger } from "@logtape/logtape"
|
||||||
|
import { exportJwk, generateCryptoKeyPair, KvStore, MessageQueue } from "@fedify/fedify"
|
||||||
|
import { PostgresMessageQueue } from "@fedify/postgres"
|
||||||
|
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 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, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
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[]>> {
|
||||||
|
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.fatal`!!! Re-initializing database from scratch for development !!!`
|
||||||
|
|
||||||
|
l.info`01/XX Dropping public schema from database...`
|
||||||
|
await this.#doQueryFile("src/database/init/01-drop-schema.sql")
|
||||||
|
|
||||||
|
l.info`02/XX Recreating public schema...`
|
||||||
|
await this.#doQueryFile("src/database/init/02-create-schema.sql")
|
||||||
|
|
||||||
|
l.info`03/XX Creating actor table...`
|
||||||
|
await this.#doQueryFile("src/database/init/03-create-actor.sql")
|
||||||
|
|
||||||
|
l.info`04/XX Creating service actor...`
|
||||||
|
l.debug`Creating RSA key pair...`
|
||||||
|
const {publicKey: publicRsa, privateKey: privateRsa} = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5")
|
||||||
|
l.debug`Creating elliptic curve key pair...`
|
||||||
|
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<DbActor>(
|
||||||
|
"src/database/init/04-insert-actor-app.sql",
|
||||||
|
[
|
||||||
|
publicRsaJwk,
|
||||||
|
publicEdJwk,
|
||||||
|
privateRsaJwk,
|
||||||
|
privateEdJwk,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
l.info`05/XX Creating follow table...`
|
||||||
|
await this.#doQueryFile("src/database/init/05-create-follow.sql")
|
||||||
|
|
||||||
|
l.info("Done!")
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActorApp(): Promise<DbActor> {
|
||||||
|
l.info`Getting app actor entity...`
|
||||||
|
|
||||||
|
const results = await this.#doQueryFile<DbActor>("src/database/query/get-actor_app.sql")
|
||||||
|
|
||||||
|
if(results.count == 0) {
|
||||||
|
l.error`No app actor entity exists.`
|
||||||
|
throw new Error("No app actor entity exists.")
|
||||||
|
}
|
||||||
|
else if(results.count > 1) {
|
||||||
|
l.error`Multiple app actor entities exist.`
|
||||||
|
throw new Error("Multiple app 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<DbActor | null> {
|
||||||
|
l.info`Getting player actor entity with id: ${steamId}`
|
||||||
|
|
||||||
|
const results = await this.#doQueryFile<DbActor>("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<DbActor | null> {
|
||||||
|
l.info`Getting guild actor entity with id: ${guildId}`
|
||||||
|
|
||||||
|
const results = await this.#doQueryFile<DbActor>("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
|
||||||
|
}
|
||||||
|
|
||||||
|
async putFollow(follower: string, followed: string): Promise<void> {
|
||||||
|
l.info`Putting follow: from ${follower} to ${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])
|
||||||
|
}
|
||||||
|
}
|
1
src/database/init/01-drop-schema.sql
Normal file
1
src/database/init/01-drop-schema.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DROP SCHEMA public CASCADE;
|
1
src/database/init/02-create-schema.sql
Normal file
1
src/database/init/02-create-schema.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
CREATE SCHEMA public;
|
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
|
||||||
|
);
|
13
src/database/init/04-insert-actor-app.sql
Normal file
13
src/database/init/04-insert-actor-app.sql
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
INSERT INTO actor (
|
||||||
|
handle,
|
||||||
|
public_rsa,
|
||||||
|
public_ed,
|
||||||
|
private_rsa,
|
||||||
|
private_ed
|
||||||
|
) VALUES (
|
||||||
|
'app',
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4
|
||||||
|
);
|
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
|
||||||
|
)
|
||||||
|
);
|
6
src/database/query/delete-follow.sql
Normal file
6
src/database/query/delete-follow.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
DELETE FROM
|
||||||
|
follow
|
||||||
|
WHERE
|
||||||
|
follow.follower = $1
|
||||||
|
AND
|
||||||
|
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';
|
10
src/database/query/get-actor_guild.sql
Normal file
10
src/database/query/get-actor_guild.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 = 'g' || $1;
|
10
src/database/query/get-actor_player.sql
Normal file
10
src/database/query/get-actor_player.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 = 'u' || $1;
|
7
src/database/query/put-follow.sql
Normal file
7
src/database/query/put-follow.sql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
INSERT INTO follow (
|
||||||
|
follower,
|
||||||
|
followed
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2
|
||||||
|
) ON CONFLICT DO NOTHING;
|
18
src/deno/logging.ts
Normal file
18
src/deno/logging.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { configure, getConsoleSink, getLogger } from "@logtape/logtape"
|
||||||
|
|
||||||
|
|
||||||
|
const l = getLogger(["dotino-veloce", "deno", "logging"])
|
||||||
|
|
||||||
|
|
||||||
|
export async function initLogging() {
|
||||||
|
await configure({
|
||||||
|
sinks: { console: getConsoleSink() },
|
||||||
|
filters: {},
|
||||||
|
loggers: [
|
||||||
|
{ category: ["logtape", "meta"], sinks: ["console"], level: "warning" },
|
||||||
|
{ category: ["fedify"], sinks: ["console"], level: "debug" },
|
||||||
|
{ category: ["dotino-veloce"], sinks: ["console"], level: "debug" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
l.info`Logging initialized successfully!`
|
||||||
|
}
|
45
src/deno/router.ts
Normal file
45
src/deno/router.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { getLogger } from "@logtape/logtape"
|
||||||
|
import { handleHostMeta } from "../dv/hostMeta.ts"
|
||||||
|
import { DotinoVeloce } from "../dv/index.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`Determining request's URL...`
|
||||||
|
const url = new URL(request.url)
|
||||||
|
|
||||||
|
l.debug`Determining request's pathname...`
|
||||||
|
const pathname = url.pathname
|
||||||
|
|
||||||
|
l.info`User-Agent: ${agent} | URL: ${url} | Pathname: ${pathname}`
|
||||||
|
|
||||||
|
if (url.pathname === "/favicon.ico") {
|
||||||
|
l.debug`Delegating handling to favicon handler...`
|
||||||
|
return await handleFavicon()
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
31
src/deno/server.ts
Normal file
31
src/deno/server.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts"
|
||||||
|
|
||||||
|
|
||||||
|
const l = getLogger(["dotino-veloce", "deno", "server"])
|
||||||
|
|
||||||
|
|
||||||
|
export function doServe(router: Deno.ServeHandler) {
|
||||||
|
l.info`Starting server...`
|
||||||
|
Deno.serve(
|
||||||
|
{
|
||||||
|
port: 8080,
|
||||||
|
|
||||||
|
onListen(localAddr) {
|
||||||
|
l.info`Listening on: ${localAddr}`
|
||||||
|
},
|
||||||
|
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
async onError(error) {
|
||||||
|
l.error`Error caught at the serve boundary: ${error}`
|
||||||
|
return new Response(
|
||||||
|
"An internal server error has occurred.",
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
router,
|
||||||
|
)
|
||||||
|
}
|
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
|
||||||
|
}
|
||||||
|
}
|
18
src/dv/favicon.ts
Normal file
18
src/dv/favicon.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { getLogger } from "@logtape/logtape"
|
||||||
|
|
||||||
|
|
||||||
|
const l = getLogger(["dotino-veloce", "dv", "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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
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!`
|
||||||
|
}
|
||||||
|
}
|
21
src/dv/hostMeta.ts
Normal file
21
src/dv/hostMeta.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { getLogger } from "@logtape/logtape"
|
||||||
|
|
||||||
|
|
||||||
|
const l = getLogger(["dotino-veloce", "dv", "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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
62
src/dv/index.ts
Normal file
62
src/dv/index.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { createFederation, Federation, Follow, Undo } from "@fedify/fedify"
|
||||||
|
import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts"
|
||||||
|
import { StratzAPI } from "../stratz/api.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"])
|
||||||
|
|
||||||
|
|
||||||
|
export type ContextData = undefined
|
||||||
|
|
||||||
|
|
||||||
|
export class DotinoVeloce {
|
||||||
|
db: Database
|
||||||
|
stratz: StratzAPI
|
||||||
|
federation: Federation<ContextData>
|
||||||
|
|
||||||
|
cNodeInfo: NodeInfoComponent
|
||||||
|
cActor: ActorComponent
|
||||||
|
cKey: KeyComponent
|
||||||
|
cFollow: FollowComponent
|
||||||
|
cUndo: UndoComponent
|
||||||
|
|
||||||
|
constructor(db: Database, stratz: StratzAPI) {
|
||||||
|
this.db = db
|
||||||
|
this.stratz = stratz
|
||||||
|
|
||||||
|
this.cNodeInfo = new NodeInfoComponent(this)
|
||||||
|
this.cActor = new ActorComponent(this)
|
||||||
|
this.cKey = new KeyComponent(this)
|
||||||
|
this.cFollow = new FollowComponent(this)
|
||||||
|
this.cUndo = new UndoComponent(this)
|
||||||
|
|
||||||
|
this.federation = createFederation<ContextData>({
|
||||||
|
kv: db.useAsKvStore(),
|
||||||
|
queue: db.useAsMessageQueue(),
|
||||||
|
})
|
||||||
|
|
||||||
|
this.federation
|
||||||
|
.setActorDispatcher("/users/{identifier}", this.cActor.getActor.bind(this.cActor))
|
||||||
|
.setKeyPairsDispatcher(this.cKey.getActorKeys.bind(this.cKey))
|
||||||
|
.mapHandle((_, h) => h)
|
||||||
|
|
||||||
|
this.federation
|
||||||
|
.setInboxListeners("/inbox/{identifier}", "/inbox")
|
||||||
|
// Akkoma with Authorized Fetch requires this to be set
|
||||||
|
// https://p.junimo.party/#/junimo.party/s/AnFW6s3OURPMY04LKq?view=full
|
||||||
|
.setSharedKeyDispatcher(this.cKey.getFetchKey.bind(this.cKey))
|
||||||
|
.on(Follow, this.cFollow.handlePlus.bind(this.cFollow))
|
||||||
|
.on(Undo, this.cUndo.handle.bind(this.cUndo))
|
||||||
|
|
||||||
|
this.federation
|
||||||
|
.setNodeInfoDispatcher("/nodeinfo/2.1", this.cNodeInfo.handle.bind(this.cNodeInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
// REMINDER: A pattern matcher on received activities could be useful, actually.
|
||||||
|
}
|
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}`
|
||||||
|
}
|
||||||
|
}
|
12
src/entry/dbInit.ts
Normal file
12
src/entry/dbInit.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { Database } from "../database/index.ts"
|
||||||
|
import { initLogging } from "../deno/logging.ts"
|
||||||
|
|
||||||
|
|
||||||
|
async function entryDbInit() {
|
||||||
|
await initLogging()
|
||||||
|
const db = Database.fromEnv()
|
||||||
|
await db.reinitializeDev()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
entryDbInit()
|
21
src/entry/server.ts
Normal file
21
src/entry/server.ts
Normal 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/index.ts"
|
||||||
|
import { Database } from "../database/index.ts"
|
||||||
|
|
||||||
|
|
||||||
|
async function entryServer() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
entryServer()
|
|
@ -1,140 +0,0 @@
|
||||||
import { createFederation, Person, Application, Image, PropertyValue, Organization, Group } from "@fedify/fedify"
|
|
||||||
import { kv } from "./redis.ts"
|
|
||||||
import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts"
|
|
||||||
import { doQueryGuild, doQueryPlayer } from "./graphql/stratz.ts"
|
|
||||||
import { escapeHtml } from "@@x/escape"
|
|
||||||
|
|
||||||
const l = getLogger(["dotino-veloce", "federation"])
|
|
||||||
|
|
||||||
l.debug`Creating federation object...`
|
|
||||||
export const federation = createFederation<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,99 +0,0 @@
|
||||||
import { getLogger } from "@logtape/logtape"
|
|
||||||
|
|
||||||
const l = getLogger(["dotino-veloce", "graphql"])
|
|
||||||
|
|
||||||
l.debug`Getting Stratz API URL from environment variable DOTINO_STRATZ_URL...`
|
|
||||||
const urlString = Deno.env.get("DOTINO_STRATZ_URL")
|
|
||||||
if(!urlString) {
|
|
||||||
l.error`DOTINO_STRATZ_URL is unset.`
|
|
||||||
throw new Error("DOTINO_STRATZ_URL is unset.")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.debug`Attempting to parse Stratz API URL...`
|
|
||||||
const url = new URL(urlString)
|
|
||||||
|
|
||||||
l.debug`Getting Stratz API key from environment variable DOTINO_STRATZ_KEY...`
|
|
||||||
const key = Deno.env.get("DOTINO_STRATZ_KEY")
|
|
||||||
if(!key) {
|
|
||||||
l.error`DOTINO_STRATZ_KEY is unset.`
|
|
||||||
throw new Error("DOTINO_STRATZ_KEY is unset.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Little trick to get VSCode to highlight queries!
|
|
||||||
const graphql = String.raw
|
|
||||||
|
|
||||||
export async function doQuery(query: string, variables: object) {
|
|
||||||
const request = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Authorization": `Bearer ${key}`,
|
|
||||||
"User-Agent": "STRATZ_API",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({query, variables})
|
|
||||||
})
|
|
||||||
const data = await request.json()
|
|
||||||
|
|
||||||
if(data["error"]) {
|
|
||||||
throw new Error("GraphQL query failed.", data["error"])
|
|
||||||
}
|
|
||||||
|
|
||||||
return data["data"]
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function doQueryPlayer(steamId: number): Promise<{
|
|
||||||
communityVisibleState: number,
|
|
||||||
isAnonymous: boolean,
|
|
||||||
id: number,
|
|
||||||
name: string,
|
|
||||||
avatar: string,
|
|
||||||
} | null> {
|
|
||||||
l.info`Querying player ${steamId} on the Stratz API...`
|
|
||||||
const response = await doQuery(
|
|
||||||
graphql`
|
|
||||||
query ($steamId: Long!) {
|
|
||||||
player (steamAccountId: $steamId) {
|
|
||||||
steamAccount {
|
|
||||||
communityVisibleState
|
|
||||||
isAnonymous
|
|
||||||
id
|
|
||||||
name
|
|
||||||
avatar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{steamId}
|
|
||||||
)
|
|
||||||
return response?.player?.steamAccount ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function doQueryGuild(guildId: number): Promise<{
|
|
||||||
id: number,
|
|
||||||
motd: string,
|
|
||||||
name: string,
|
|
||||||
tag: string,
|
|
||||||
logo: string,
|
|
||||||
description: string,
|
|
||||||
createdDateTime: number,
|
|
||||||
} | null> {
|
|
||||||
l.info`Querying guild ${guildId} on the Stratz API...`
|
|
||||||
const response = await doQuery(
|
|
||||||
graphql`
|
|
||||||
query ($guildId: Int!) {
|
|
||||||
guild(id: $guildId) {
|
|
||||||
id,
|
|
||||||
motd,
|
|
||||||
name,
|
|
||||||
tag,
|
|
||||||
logo,
|
|
||||||
description,
|
|
||||||
createdDateTime,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{guildId}
|
|
||||||
)
|
|
||||||
return response?.guild ?? null
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { getLogger } from "@logtape/logtape"
|
|
||||||
import { behindProxy, Fetch } from "@hongminhee/x-forwarded-fetch"
|
|
||||||
import { federation } from "./federation.ts"
|
|
||||||
|
|
||||||
const l = getLogger(["dotino-veloce", "handler"])
|
|
||||||
|
|
||||||
l.debug`Creating Deno handler...`
|
|
||||||
function handler(request: Request, _info: Deno.ServeHandlerInfo) {
|
|
||||||
l.debug`Received a request, processing...`
|
|
||||||
|
|
||||||
const agent = request.headers.get("User-Agent")
|
|
||||||
const requestUrl = new URL(request.url)
|
|
||||||
l.debug`Received request from ${agent} to ${requestUrl.href}`
|
|
||||||
|
|
||||||
// Akkoma expects host-meta to be correctly setup
|
|
||||||
// https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/web_finger.ex#L177
|
|
||||||
l.debug`Routing request to: ${requestUrl.pathname}`
|
|
||||||
if (requestUrl.pathname === "/.well-known/host-meta") {
|
|
||||||
l.debug`Intercepting request to inject host-meta for ${requestUrl.origin}`
|
|
||||||
return new Response(
|
|
||||||
`<?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.
|
|
24
src/main.ts
24
src/main.ts
|
@ -1,24 +0,0 @@
|
||||||
import { configure, getConsoleSink, getLogger } from "@logtape/logtape"
|
|
||||||
import { proxyHandler } from "./handler.ts"
|
|
||||||
|
|
||||||
await configure({
|
|
||||||
sinks: { console: getConsoleSink() },
|
|
||||||
filters: {},
|
|
||||||
loggers: [
|
|
||||||
{ category: ["logtape", "meta"], sinks: ["console"], level: "warning" },
|
|
||||||
{ category: ["fedify"], sinks: ["console"], level: "info" },
|
|
||||||
{ category: ["dotino-veloce"], sinks: ["console"], level: "debug" },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
const l = getLogger(["dotino-veloce", "main"])
|
|
||||||
|
|
||||||
l.info`Starting server...`
|
|
||||||
Deno.serve(
|
|
||||||
{
|
|
||||||
port: 8080,
|
|
||||||
onListen: (_localAddr) => {
|
|
||||||
},
|
|
||||||
},
|
|
||||||
proxyHandler,
|
|
||||||
)
|
|
11
src/redis.ts
11
src/redis.ts
|
@ -1,11 +0,0 @@
|
||||||
import { RedisKvStore } from "@fedify/redis"
|
|
||||||
import { getLogger } from "@logtape/logtape"
|
|
||||||
import { Redis } from "@@npm/ioredis"
|
|
||||||
|
|
||||||
const l = getLogger(["dotino-veloce", "redis"])
|
|
||||||
|
|
||||||
l.debug`Creating redis object...`
|
|
||||||
export const redis = new Redis({})
|
|
||||||
|
|
||||||
l.debug`Creating federation object...`
|
|
||||||
export const kv = new RedisKvStore(redis, {})
|
|
143
src/stratz/api.ts
Normal file
143
src/stratz/api.ts
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import { getLogger } from "@logtape/logtape"
|
||||||
|
|
||||||
|
|
||||||
|
const l = getLogger(["dotino-veloce", "stratz", "api"])
|
||||||
|
const graphql = String.raw
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryPlayer = {
|
||||||
|
communityVisibleState: number,
|
||||||
|
isAnonymous: boolean,
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
avatar: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueryGuild = {
|
||||||
|
id: number,
|
||||||
|
motd: string,
|
||||||
|
name: string,
|
||||||
|
tag: string,
|
||||||
|
logo: string,
|
||||||
|
description: string,
|
||||||
|
createdDateTime: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StratzAPI {
|
||||||
|
url: URL
|
||||||
|
key: string
|
||||||
|
|
||||||
|
constructor(url: string, key: string) {
|
||||||
|
l.info`Creating new Stratz API client sending requests at: ${url}`
|
||||||
|
|
||||||
|
l.debug`Using Stratz API key: ${key}`
|
||||||
|
this.key = key
|
||||||
|
|
||||||
|
l.debug`Attempting to parse Stratz API URL...`
|
||||||
|
this.url = new URL(url)
|
||||||
|
l.debug`Stratz API URL is: ${this.url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromEnv(): StratzAPI {
|
||||||
|
l.debug`Getting Stratz API URL from environment variable DOTINO_STRATZ_URL...`
|
||||||
|
const url = Deno.env.get("DOTINO_STRATZ_URL")
|
||||||
|
if(!url) {
|
||||||
|
l.error`DOTINO_STRATZ_URL is unset.`
|
||||||
|
throw new Error("DOTINO_STRATZ_URL is unset.")
|
||||||
|
}
|
||||||
|
|
||||||
|
l.debug`Getting Stratz API key from environment variable DOTINO_STRATZ_KEY...`
|
||||||
|
const key = Deno.env.get("DOTINO_STRATZ_KEY")
|
||||||
|
if(!key) {
|
||||||
|
l.error`DOTINO_STRATZ_KEY is unset.`
|
||||||
|
throw new Error("DOTINO_STRATZ_KEY is unset.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StratzAPI(url, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
async doQuery(query: string, variables: object): Promise<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/graphql/stratz.ts"
|
import { doQueryPlayer } from "../src/stratz/api.ts"
|
||||||
import { assert, assertEquals} from "@std/assert"
|
import { assert, assertEquals} from "@std/assert"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue