Compare commits
No commits in common. "42f48fb0468408b1da11aea9dbb1fe49ff535c7e" and "cad4c59d51f7ca67a1f15d546a3b859bc93b9532" have entirely different histories.
42f48fb046
...
cad4c59d51
17 changed files with 132 additions and 1982 deletions
|
@ -1 +0,0 @@
|
||||||
build/
|
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"es2021": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
}
|
|
||||||
}
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,3 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
build/
|
||||||
.env
|
.env
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
build/
|
|
19
.swcrc
19
.swcrc
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"jsc": {
|
|
||||||
"parser": {
|
|
||||||
"syntax": "typescript",
|
|
||||||
"tsx": false,
|
|
||||||
"decorators": true,
|
|
||||||
"dynamicImport": true
|
|
||||||
},
|
|
||||||
"target": "es2020",
|
|
||||||
"paths": {
|
|
||||||
"~/*": ["./src/*"],
|
|
||||||
"~entities/*": ["./src/db/entities/*"]
|
|
||||||
},
|
|
||||||
"baseUrl": "."
|
|
||||||
},
|
|
||||||
"module": {
|
|
||||||
"type": "es6"
|
|
||||||
}
|
|
||||||
}
|
|
20
README.md
20
README.md
|
@ -1,21 +1 @@
|
||||||
# Mercury
|
# Mercury
|
||||||
|
|
||||||
ActivityPub server implementation (hopefully, eventually),
|
|
||||||
currently mostly a way for me to play around with writing an API server in TypeScript.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
For now, check out `src/config.ts`, the names should be mostly self explanatory.
|
|
||||||
Consider all environment variables required.
|
|
||||||
|
|
||||||
## Development commands
|
|
||||||
|
|
||||||
- Building the server: `pnpm build`
|
|
||||||
- Migrating the database: `pnpm migrate`
|
|
||||||
- Watching for changes and reloading the server automatically: `pnpm dev`
|
|
||||||
- Formatting: `pnpm format`
|
|
||||||
- Linting (if you don't have an ESLint plugin): `pnpm format`
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Mercury is licensed under the GNU Affero General Public License, version 3 **only**.
|
|
||||||
|
|
18
package.json
18
package.json
|
@ -8,12 +8,8 @@
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"pnpm watch:build\" \"pnpm watch:dev\"",
|
"dev": "nodemon ./src/index.ts",
|
||||||
"build": "swc src -d dist",
|
"build": "tsc",
|
||||||
"watch:build": "swc src -w --out-dir dist",
|
|
||||||
"watch:dev": "nodemon --watch \"dist/**/*\" -e js ./dist/index.js",
|
|
||||||
"format": "prettier -w",
|
|
||||||
"lint": "eslint src/",
|
|
||||||
"typeorm": "typeorm-ts-node-esm",
|
"typeorm": "typeorm-ts-node-esm",
|
||||||
"migrate": "typeorm-ts-node-esm migration:run -d ./src/db/index.ts"
|
"migrate": "typeorm-ts-node-esm migration:run -d ./src/db/index.ts"
|
||||||
},
|
},
|
||||||
|
@ -21,8 +17,6 @@
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"fastify": "^4.20.0",
|
|
||||||
"glob": "^10.3.3",
|
|
||||||
"pg": "^8.11.1",
|
"pg": "^8.11.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"tslog": "^4.8.2",
|
"tslog": "^4.8.2",
|
||||||
|
@ -30,16 +24,10 @@
|
||||||
"ulid": "^2.3.0"
|
"ulid": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@swc/cli": "^0.1.62",
|
|
||||||
"@swc/core": "^1.3.70",
|
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
|
||||||
"@typescript-eslint/parser": "^6.1.0",
|
|
||||||
"chokidar": "^3.5.3",
|
|
||||||
"concurrently": "^8.2.0",
|
|
||||||
"eslint": "^8.45.0",
|
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1867
pnpm-lock.yaml
generated
1867
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -9,10 +9,3 @@ export const DATABASE_PORT = Number(process.env.DATABASE_PORT) || 5432;
|
||||||
export const DATABASE_USER = process.env.DATABASE_USER || "postgres";
|
export const DATABASE_USER = process.env.DATABASE_USER || "postgres";
|
||||||
export const DATABASE_PASS = process.env.DATABASE_PASS || "postgres";
|
export const DATABASE_PASS = process.env.DATABASE_PASS || "postgres";
|
||||||
export const DATABASE_NAME = process.env.DATABASE_NAME || "postgres";
|
export const DATABASE_NAME = process.env.DATABASE_NAME || "postgres";
|
||||||
|
|
||||||
export const HTTPS = process.env.HTTPS === "true";
|
|
||||||
export const DOMAIN = process.env.DOMAIN;
|
|
||||||
|
|
||||||
if (!DOMAIN) throw "$DOMAIN is empty";
|
|
||||||
|
|
||||||
export const BASE_URL = `${HTTPS ? "https" : "http"}://${DOMAIN}`;
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
import { hash } from "argon2";
|
import { hash } from "argon2";
|
||||||
|
|
||||||
import { Account } from "~entities/account.js";
|
import { Account } from "./entities/account.js";
|
||||||
import MercuryDataSource from "./index.js";
|
import MercuryDataSource from "./index.js";
|
||||||
|
|
||||||
const missingAuthData = new Error("missing auth data for local user");
|
const missingAuthData = new Error("missing auth data for local user");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
|
|
||||||
import { Account } from "~entities/account.js";
|
import { Account } from "./entities/account.js";
|
||||||
import { Blog } from "~entities/blog.js";
|
import { Blog } from "./entities/blog.js";
|
||||||
import { generateKeyPair } from "./util/rsa.js";
|
import { generateKeyPair } from "./util/rsa.js";
|
||||||
import MercuryDataSource from "./index.js";
|
import MercuryDataSource from "./index.js";
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { DataSource } from "typeorm";
|
import { DataSource } from "typeorm";
|
||||||
import * as config from "~/config.js";
|
import * as config from "../config.js";
|
||||||
|
|
||||||
// Entity types
|
// Entity types
|
||||||
import { Account } from "~entities/account.js";
|
import { Account } from "./entities/account.js";
|
||||||
import { Blog } from "~entities/blog.js";
|
import { Blog } from "./entities/blog.js";
|
||||||
import { Post } from "~entities/post.js";
|
import { Post } from "./entities/post.js";
|
||||||
|
|
||||||
const MercuryDataSource = new DataSource({
|
const MercuryDataSource = new DataSource({
|
||||||
type: "postgres",
|
type: "postgres",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as crypto from "node:crypto";
|
import * as crypto from "node:crypto";
|
||||||
import { promisify }from "node:util";
|
import * as util from "node:util";
|
||||||
|
|
||||||
const generate = promisify(crypto.generateKeyPair);
|
const generate = util.promisify(crypto.generateKeyPair);
|
||||||
|
|
||||||
export async function generateKeyPair() {
|
export async function generateKeyPair() {
|
||||||
return await generate("rsa", {
|
return await generate("rsa", {
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { dirname, join } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { glob } from "glob";
|
|
||||||
import type { FastifyInstance, RouteOptions } from "fastify";
|
|
||||||
|
|
||||||
import log from "./log.js";
|
|
||||||
|
|
||||||
export default async function getRoutes() {
|
|
||||||
const rootDir = dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
const routes: RouteOptions[] = [];
|
|
||||||
const matches = await glob(join(rootDir, "/routes/**/*.{js,ts}"));
|
|
||||||
|
|
||||||
for (const filename of matches) {
|
|
||||||
try {
|
|
||||||
const mod = await import(filename);
|
|
||||||
routes.push(mod.default as RouteOptions);
|
|
||||||
} catch (e) {
|
|
||||||
log.error("Importing route %s", filename, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return routes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mountRoutes(app: FastifyInstance, routes: RouteOptions[]) {
|
|
||||||
for (const route of routes) {
|
|
||||||
log.trace("Mounting route %s %s", route.method, route.url);
|
|
||||||
app.route(route);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
import type { FastifyReply, FastifyRequest, RouteOptions } from "fastify";
|
|
||||||
import { IsNull } from "typeorm";
|
|
||||||
|
|
||||||
import log from "~/log.js";
|
|
||||||
import MercuryDataSource from "~/db/index.js";
|
|
||||||
import { Blog } from "~entities/blog.js";
|
|
||||||
import { BASE_URL } from "~/config.js";
|
|
||||||
|
|
||||||
const route: RouteOptions = {
|
|
||||||
method: "GET",
|
|
||||||
url: "/.well-known/webfinger",
|
|
||||||
handler: async (req, res) => {
|
|
||||||
// TypeScript complains if we just use plain `req.query` :(
|
|
||||||
const encodedResource = (req.query as { resource: string }).resource;
|
|
||||||
|
|
||||||
if (!encodedResource || typeof encodedResource !== "string") {
|
|
||||||
res.status(400).send({
|
|
||||||
error: "resource query parameter is missing or invalid",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const resource = decodeURIComponent(encodedResource);
|
|
||||||
|
|
||||||
log.debug("Handling WebFinger request for %s", resource);
|
|
||||||
|
|
||||||
if (resource.startsWith("acct:")) {
|
|
||||||
await handleAcctWebfinger(req, res, resource);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(500).send({ error: "Unhandled WebFinger resource type" });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default route;
|
|
||||||
|
|
||||||
async function handleAcctWebfinger(
|
|
||||||
req: FastifyRequest,
|
|
||||||
res: FastifyReply,
|
|
||||||
resource: string
|
|
||||||
): Promise<void> {
|
|
||||||
const [username, domain] = resource.slice("acct:".length).split("@");
|
|
||||||
if (domain !== process.env.DOMAIN) {
|
|
||||||
res.status(404).send({
|
|
||||||
error: "Account not found",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blog = await MercuryDataSource.getRepository(Blog).findOneBy({
|
|
||||||
username: username,
|
|
||||||
host: IsNull(),
|
|
||||||
});
|
|
||||||
if (!blog) {
|
|
||||||
res.status(404).send({
|
|
||||||
error: "Account not found",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send({
|
|
||||||
subject: resource,
|
|
||||||
aliases: [
|
|
||||||
`${BASE_URL}/@${blog.username}`,
|
|
||||||
`${BASE_URL}/blogs/${blog.username}`,
|
|
||||||
],
|
|
||||||
links: [
|
|
||||||
{
|
|
||||||
rel: "http://webfinger.net/rel/profile-page",
|
|
||||||
type: "text/html",
|
|
||||||
href: `${BASE_URL}/@${blog.username}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rel: "self",
|
|
||||||
type: "application/activity+json",
|
|
||||||
href: `${BASE_URL}/blogs/${blog.username}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
22
src/start.ts
22
src/start.ts
|
@ -1,10 +1,9 @@
|
||||||
import "reflect-metadata"; // Required for TypeORM
|
import "reflect-metadata"; // Required for TypeORM
|
||||||
import Fastify from "fastify";
|
import express from "express";
|
||||||
|
|
||||||
import MercuryDataSource from "./db/index.js";
|
import MercuryDataSource from "./db/index.js";
|
||||||
import log from "./log.js";
|
import log from "./log.js";
|
||||||
|
import { Blog } from "./db/entities/blog.js";
|
||||||
import { PORT } from "./config.js";
|
import { PORT } from "./config.js";
|
||||||
import getRoutes, { mountRoutes } from "./routes.js";
|
|
||||||
|
|
||||||
export default async function start() {
|
export default async function start() {
|
||||||
log.info("Initializing database");
|
log.info("Initializing database");
|
||||||
|
@ -18,12 +17,17 @@ export default async function start() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("Setting up routes");
|
log.debug("Setting up routes")
|
||||||
const app = Fastify();
|
const app = express();
|
||||||
|
|
||||||
const routes = await getRoutes();
|
app.get("/", async (_, res) => {
|
||||||
mountRoutes(app, routes);
|
const blogRepository = MercuryDataSource.getRepository(Blog);
|
||||||
|
|
||||||
log.info("Listening on port %d", PORT);
|
const resp = await blogRepository.find();
|
||||||
await app.listen({ port: PORT });
|
|
||||||
|
return res.json(resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
log.info("Listening on port %d", PORT)
|
||||||
|
app.listen(PORT);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"declaration": false,
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./build",
|
"outDir": "./build",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
@ -16,10 +17,5 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"baseUrl": "./",
|
|
||||||
"paths": {
|
|
||||||
"~/*": ["./src/*"],
|
|
||||||
"~entities/*": ["./src/db/entities/*"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue