Compare commits
No commits in common. "37d414314e16fae1b21fd4dd5838f12eac2c9a06" and "8b40d7e04251c7215c112dac1f674730554be53b" have entirely different histories.
37d414314e
...
8b40d7e042
13 changed files with 5 additions and 200 deletions
|
@ -16,15 +16,6 @@ Consider all environment variables required.
|
||||||
- Formatting: `pnpm format`
|
- Formatting: `pnpm format`
|
||||||
- Linting (if you don't have an ESLint plugin): `pnpm format`
|
- Linting (if you don't have an ESLint plugin): `pnpm format`
|
||||||
|
|
||||||
## Creating migrations
|
|
||||||
|
|
||||||
Creating migrations is a little awkward because TypeORM expects `ts-node`, which uses `tsc`.
|
|
||||||
To create a migration, first run `pnpm build`,
|
|
||||||
then run `pnpm typeorm migration:generate -p -d ./dist/db/index.js ./src/db/migrations/<name>`,
|
|
||||||
replacing `<name>` with the name of the migration you're creating.
|
|
||||||
Then rename the created file to end in `.js` and remove the TypeScript-specific code from it.
|
|
||||||
(Yes, this is incredibly janky, but it works, and it only needs to be done once per migration, actually migrating works flawlessly)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Mercury is licensed under the GNU Affero General Public License, version 3 **only**.
|
Mercury is licensed under the GNU Affero General Public License, version 3 **only**.
|
||||||
|
|
|
@ -14,8 +14,8 @@
|
||||||
"watch:dev": "nodemon --watch \"dist/**/*\" -e js ./dist/index.js",
|
"watch:dev": "nodemon --watch \"dist/**/*\" -e js ./dist/index.js",
|
||||||
"format": "prettier -w",
|
"format": "prettier -w",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"typeorm": "typeorm",
|
"typeorm": "typeorm-ts-node-esm",
|
||||||
"migrate": "pnpm build && typeorm migration:run -d ./dist/db/index.js"
|
"migrate": "typeorm-ts-node-esm migration:run -d ./src/db/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { BASE_URL } from "~/config.js";
|
|
||||||
import { Blog } from "~/db/entities/blog.js";
|
|
||||||
|
|
||||||
/** Transforms the given Blog into an ActivityPub Person. It is the caller's responsibility to ensure the blog is local. */
|
|
||||||
export function blogToActivityPub(blog: Blog) {
|
|
||||||
return {
|
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
|
||||||
type: "Person",
|
|
||||||
id: `${BASE_URL}/blogs/${blog.username}`,
|
|
||||||
inbox: `${BASE_URL}/blogs/${blog.username}/inbox`,
|
|
||||||
outbox: `${BASE_URL}/blogs/${blog.username}/outbox`,
|
|
||||||
name: blog.username,
|
|
||||||
preferredUsername: blog.username,
|
|
||||||
publicKey: {
|
|
||||||
id: `${BASE_URL}/blogs/${blog.username}#main-key`,
|
|
||||||
owner: `${BASE_URL}/blogs/${blog.username}`,
|
|
||||||
publicKeyPem: blog.publicKey,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -5,11 +5,11 @@ 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";
|
||||||
|
|
||||||
export const incorrectAccountType = new Error("wrong account type for function");
|
export const notLocalAccount = new Error("account is not local");
|
||||||
|
|
||||||
/** Create a local blog. Throws an error if the given account is not a local account. */
|
/** Create a local blog. Throws an error if the given account is not a local account. */
|
||||||
export async function createLocalBlog(account: Account, name: string) {
|
export async function createLocalBlog(account: Account, name: string) {
|
||||||
if (account.host) throw incorrectAccountType;
|
if (account.host) throw notLocalAccount;
|
||||||
|
|
||||||
const keyPair = await generateKeyPair();
|
const keyPair = await generateKeyPair();
|
||||||
|
|
||||||
|
@ -25,19 +25,3 @@ export async function createLocalBlog(account: Account, name: string) {
|
||||||
|
|
||||||
return blog;
|
return blog;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createRemoteBlog(account: Account, name: string, apId: string, publicKey: string) {
|
|
||||||
if (!account.host) throw incorrectAccountType;
|
|
||||||
|
|
||||||
const blog = new Blog();
|
|
||||||
blog.id = ulid();
|
|
||||||
blog.apId = apId;
|
|
||||||
blog.username = name;
|
|
||||||
blog.host = account.host;
|
|
||||||
blog.account = account;
|
|
||||||
blog.publicKey = publicKey;
|
|
||||||
|
|
||||||
await MercuryDataSource.getRepository(Blog).save(blog);
|
|
||||||
|
|
||||||
return blog;
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,8 +14,6 @@ import { Post } from "./post.js";
|
||||||
export class Blog {
|
export class Blog {
|
||||||
@PrimaryColumn("text")
|
@PrimaryColumn("text")
|
||||||
id: string;
|
id: string;
|
||||||
@Column("text", { nullable: true, unique: true, comment: "ActivityPub ID" })
|
|
||||||
apId: string | null;
|
|
||||||
@Column("text", { nullable: false })
|
@Column("text", { nullable: false })
|
||||||
username: string;
|
username: string;
|
||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
|
|
|
@ -5,8 +5,6 @@ import { Blog } from "./blog.js";
|
||||||
export class Post {
|
export class Post {
|
||||||
@PrimaryColumn("text")
|
@PrimaryColumn("text")
|
||||||
id: string;
|
id: string;
|
||||||
@Column("text", { nullable: true, unique: true, comment: "ActivityPub ID" })
|
|
||||||
apId: string | null;
|
|
||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
content: string | null;
|
content: string | null;
|
||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
|
|
|
@ -15,8 +15,6 @@ const MercuryDataSource = new DataSource({
|
||||||
database: config.DATABASE_NAME,
|
database: config.DATABASE_NAME,
|
||||||
entities: [Account, Blog, Post],
|
entities: [Account, Blog, Post],
|
||||||
migrations: ["src/db/migrations/*.js"],
|
migrations: ["src/db/migrations/*.js"],
|
||||||
logging:
|
|
||||||
process.env.NODE_ENV === "production" ? ["error"] : ["query", "error"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default MercuryDataSource;
|
export default MercuryDataSource;
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
export class AddApIds1689900517279 {
|
|
||||||
name = 'AddApIds1689900517279'
|
|
||||||
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`
|
|
||||||
ALTER TABLE "post"
|
|
||||||
ADD "apId" text
|
|
||||||
`);
|
|
||||||
await queryRunner.query(`
|
|
||||||
ALTER TABLE "post"
|
|
||||||
ADD CONSTRAINT "UQ_e16e967a725a0f3f681bf99bd6e" UNIQUE ("apId")
|
|
||||||
`);
|
|
||||||
await queryRunner.query(`
|
|
||||||
COMMENT ON COLUMN "post"."apId" IS 'ActivityPub ID'
|
|
||||||
`);
|
|
||||||
await queryRunner.query(`
|
|
||||||
ALTER TABLE "blog"
|
|
||||||
ADD "apId" text
|
|
||||||
`);
|
|
||||||
await queryRunner.query(`
|
|
||||||
ALTER TABLE "blog"
|
|
||||||
ADD CONSTRAINT "UQ_624066bd60ecf91ee390637d171" UNIQUE ("apId")
|
|
||||||
`);
|
|
||||||
await queryRunner.query(`
|
|
||||||
COMMENT ON COLUMN "blog"."apId" IS 'ActivityPub ID'
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query(`
|
|
||||||
COMMENT ON COLUMN "blog"."apId" IS 'ActivityPub ID'
|
|
||||||
`);
|
|
||||||
await queryRunner.query(`
|
|
||||||
ALTER TABLE "blog" DROP CONSTRAINT "UQ_624066bd60ecf91ee390637d171"
|
|
||||||
`);
|
|
||||||
await queryRunner.query(`
|
|
||||||
ALTER TABLE "blog" DROP COLUMN "apId"
|
|
||||||
`);
|
|
||||||
await queryRunner.query(`
|
|
||||||
COMMENT ON COLUMN "post"."apId" IS 'ActivityPub ID'
|
|
||||||
`);
|
|
||||||
await queryRunner.query(`
|
|
||||||
ALTER TABLE "post" DROP CONSTRAINT "UQ_e16e967a725a0f3f681bf99bd6e"
|
|
||||||
`);
|
|
||||||
await queryRunner.query(`
|
|
||||||
ALTER TABLE "post" DROP COLUMN "apId"
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { RouteOptions } from "fastify";
|
|
||||||
|
|
||||||
import MercuryDataSource from "~/db/index.js";
|
|
||||||
import { Blog } from "~/db/entities/blog.js";
|
|
||||||
import { IsNull } from "typeorm";
|
|
||||||
import { blogToActivityPub } from "~/ap/blog.js";
|
|
||||||
|
|
||||||
const route: RouteOptions = {
|
|
||||||
method: "GET",
|
|
||||||
url: "/blogs/:username",
|
|
||||||
handler: async (req, res) => {
|
|
||||||
const username = (req.params as { username: string }).username;
|
|
||||||
|
|
||||||
// Only respond to ActivityPub requests
|
|
||||||
if (req.headers.accept && !req.headers.accept.indexOf("application/json")) {
|
|
||||||
res.redirect(303, `/@${username}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const blog = await MercuryDataSource.getRepository(Blog).findOneBy({
|
|
||||||
username,
|
|
||||||
host: IsNull(),
|
|
||||||
});
|
|
||||||
if (!blog) {
|
|
||||||
return res.status(404).send({ message: "Not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send(blogToActivityPub(blog));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default route;
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { RouteOptions } from "fastify";
|
|
||||||
import { IsNull } from "typeorm";
|
|
||||||
|
|
||||||
import MercuryDataSource from "~/db/index.js";
|
|
||||||
import { Blog } from "~/db/entities/blog.js";
|
|
||||||
import { Post } from "~/db/entities/post.js";
|
|
||||||
|
|
||||||
const route: RouteOptions = {
|
|
||||||
method: "GET",
|
|
||||||
url: "/nodeinfo/2.0",
|
|
||||||
handler: async (_, res) => {
|
|
||||||
const [userCount, postCount] = await Promise.all([
|
|
||||||
MercuryDataSource.getRepository(Blog).countBy({
|
|
||||||
host: IsNull(),
|
|
||||||
}),
|
|
||||||
MercuryDataSource.getRepository(Post).count({
|
|
||||||
relations: { blog: true },
|
|
||||||
where: { blog: { host: IsNull() } },
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
res.send({
|
|
||||||
version: "2.0",
|
|
||||||
software: { name: "mercury", version: "0.1.0-dev" },
|
|
||||||
protocols: ["activitypub"],
|
|
||||||
openRegistrations: false, // TODO: get from database
|
|
||||||
usage: {
|
|
||||||
users: {
|
|
||||||
total: userCount,
|
|
||||||
},
|
|
||||||
localPosts: postCount,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default route;
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { RouteOptions } from "fastify";
|
|
||||||
|
|
||||||
import { BASE_URL } from "~/config.js";
|
|
||||||
|
|
||||||
const route: RouteOptions = {
|
|
||||||
method: "GET",
|
|
||||||
url: "/.well-known/nodeinfo",
|
|
||||||
handler: async (_, res) => {
|
|
||||||
res.send({
|
|
||||||
links: [
|
|
||||||
{
|
|
||||||
href: `${BASE_URL}/nodeinfo/2.0`,
|
|
||||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default route;
|
|
|
@ -21,11 +21,6 @@ export default async function start() {
|
||||||
log.debug("Setting up routes");
|
log.debug("Setting up routes");
|
||||||
const app = Fastify();
|
const app = Fastify();
|
||||||
|
|
||||||
app.setNotFoundHandler((req, res) => {
|
|
||||||
log.debug("Route %s not found", req.url);
|
|
||||||
res.status(404).send({ message: "Not found" });
|
|
||||||
});
|
|
||||||
|
|
||||||
const routes = await getRoutes();
|
const routes = await getRoutes();
|
||||||
mountRoutes(app, routes);
|
mountRoutes(app, routes);
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,5 @@
|
||||||
"~/*": ["./src/*"],
|
"~/*": ["./src/*"],
|
||||||
"~entities/*": ["./src/db/entities/*"]
|
"~entities/*": ["./src/db/entities/*"]
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"exclude": ["./dist/"]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue