diff --git a/database/entities/RawActivity.ts b/database/entities/RawActivity.ts index 125116f1..9e68ac59 100644 --- a/database/entities/RawActivity.ts +++ b/database/entities/RawActivity.ts @@ -2,6 +2,7 @@ import { BaseEntity, Column, Entity, + JoinTable, ManyToMany, PrimaryGeneratedColumn, } from "typeorm"; @@ -18,10 +19,11 @@ export class RawActivity extends BaseEntity { @PrimaryGeneratedColumn("uuid") id!: string; - @Column("json") + @Column("jsonb") data!: APActivity; // Any associated objects (there is typically only one) @ManyToMany(() => RawObject, object => object.id) + @JoinTable() objects!: RawObject[]; } diff --git a/database/entities/RawObject.ts b/database/entities/RawObject.ts index 61477fc0..335941b5 100644 --- a/database/entities/RawObject.ts +++ b/database/entities/RawObject.ts @@ -11,6 +11,6 @@ export class RawObject extends BaseEntity { @PrimaryGeneratedColumn("uuid") id!: string; - @Column("json") + @Column("jsonb") data!: APObject; } diff --git a/index.ts b/index.ts index 058a7193..0d3cf121 100644 --- a/index.ts +++ b/index.ts @@ -19,6 +19,8 @@ Bun.serve({ async fetch(req) { const matchedRoute = router.match(req); + console.log(req.url); + if (matchedRoute) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call return (await import(matchedRoute.filePath)).default( diff --git a/server/api/@[username]/actor.json/index.ts b/server/api/[username]/actor.json/index.ts similarity index 98% rename from server/api/@[username]/actor.json/index.ts rename to server/api/[username]/actor.json/index.ts index 1582f03a..6f70b437 100644 --- a/server/api/@[username]/actor.json/index.ts +++ b/server/api/[username]/actor.json/index.ts @@ -11,7 +11,7 @@ export default async ( req: Request, matchedRoute: MatchedRoute ): Promise => { - const username = matchedRoute.params.username; + const username = matchedRoute.params.username.split("@")[0]; const user = await User.findOneBy({ username }); diff --git a/server/api/@[username]/inbox/index.ts b/server/api/[username]/inbox/index.ts similarity index 100% rename from server/api/@[username]/inbox/index.ts rename to server/api/[username]/inbox/index.ts diff --git a/server/api/@[username]/outbox.json/index.ts b/server/api/[username]/outbox.json/index.ts similarity index 98% rename from server/api/@[username]/outbox.json/index.ts rename to server/api/[username]/outbox.json/index.ts index d5030f73..d192aa86 100644 --- a/server/api/@[username]/outbox.json/index.ts +++ b/server/api/[username]/outbox.json/index.ts @@ -12,7 +12,7 @@ export default async ( req: Request, matchedRoute: MatchedRoute ): Promise => { - const username = matchedRoute.params.username; + const username = matchedRoute.params.username.split("@")[0]; const page = Boolean(matchedRoute.query.page || "false"); const min_id = matchedRoute.query.min_id || false; const max_id = matchedRoute.query.max_id || false; diff --git a/tests/inbox.test.ts b/tests/inbox.test.ts new file mode 100644 index 00000000..63cbe8b4 --- /dev/null +++ b/tests/inbox.test.ts @@ -0,0 +1,123 @@ +import { getConfig } from "@config"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { AppDataSource } from "~database/datasource"; +import { RawActivity } from "~database/entities/RawActivity"; +import { Token } from "~database/entities/Token"; +import { User } from "~database/entities/User"; + +const config = getConfig(); + +beforeAll(async () => { + if (!AppDataSource.isInitialized) await AppDataSource.initialize(); + + // Initialize test user + const user = new User(); + + user.email = "test@test.com"; + user.username = "test"; + user.password = await Bun.password.hash("test"); + user.display_name = ""; + user.bio = ""; + + await user.save(); +}); + +describe("POST /@test/inbox", () => { + test("should store a new Note object", async () => { + const response = await fetch( + `${config.http.base_url}:${config.http.port}/@test/inbox/`, + { + method: "POST", + headers: { + "Content-Type": "application/activity+json", + }, + body: JSON.stringify({ + "@context": "https://www.w3.org/ns/activitystreams", + type: "Create", + id: "https://example.com/notes/1/activity", + actor: `${config.http.base_url}:${config.http.port}/@test`, + to: ["https://www.w3.org/ns/activitystreams#Public"], + cc: [], + published: "2021-01-01T00:00:00.000Z", + object: { + "@context": "https://www.w3.org/ns/activitystreams", + id: "https://example.com/notes/1", + type: "Note", + content: "Hello, world!", + summary: null, + inReplyTo: null, + published: "2021-01-01T00:00:00.000Z", + }, + }), + } + ); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("application/json"); + + const activity = await RawActivity.createQueryBuilder("activity") + // id is part of the jsonb column 'data' + .where("activity.data->>'id' = :id", { + id: "https://example.com/notes/1/activity", + }) + .leftJoinAndSelect("activity.objects", "objects") + .getOne(); + + expect(activity).not.toBeUndefined(); + expect(activity?.data).toEqual({ + "@context": "https://www.w3.org/ns/activitystreams", + type: "Create", + id: "https://example.com/notes/1/activity", + actor: `${config.http.base_url}:${config.http.port}/@test`, + to: ["https://www.w3.org/ns/activitystreams#Public"], + cc: [], + published: "2021-01-01T00:00:00.000Z", + }); + + expect(activity?.objects).toHaveLength(1); + expect(activity?.objects[0].data).toEqual({ + "@context": "https://www.w3.org/ns/activitystreams", + id: "https://example.com/notes/1", + type: "Note", + content: "Hello, world!", + summary: null, + inReplyTo: null, + published: "2021-01-01T00:00:00.000Z", + }); + }); +}); + +afterAll(async () => { + // Clean up user + const user = await User.findOneBy({ + username: "test", + }); + + // Clean up tokens + const tokens = await Token.findBy({ + user: { + username: "test", + }, + }); + + const activities = await RawActivity.createQueryBuilder("activity") + .where("activity.data->>'actor' = :actor", { + actor: `${config.http.base_url}:${config.http.port}/@test`, + }) + .leftJoinAndSelect("activity.objects", "objects") + .getMany(); + + // Delete all created objects and activities as part of testing + await Promise.all( + activities.map(async activity => { + await Promise.all( + activity.objects.map(async object => await object.remove()) + ); + await activity.remove(); + }) + ); + + await Promise.all(tokens.map(async token => await token.remove())); + + if (user) await user.remove(); +});