Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: save event as staging after validation #7

Merged
merged 11 commits into from
Dec 19, 2024
3 changes: 1 addition & 2 deletions packages/core/src/events/m.room.power_levels.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ test("roomPowerLevelsEvent", async () => {
],
depth: 3,
prev_events: ["$tZRt2bwceX4sG913Ee67tJiwe-gk859kY2mCeYSncw8"],
sender: "@admin:hs1",
member: "@asd6:rc1",
members: ["@admin:hs1", "@asd6:rc1"],
ts: 1733107418713,
});

Expand Down
12 changes: 7 additions & 5 deletions packages/core/src/events/m.room.power_levels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,20 @@ interface RoomPowerLevelsEvent extends EventBase {

export const roomPowerLevelsEvent = ({
roomId,
sender,
member,
members: usernames,
auth_events,
prev_events,
depth,
ts = Date.now(),
}: {
roomId: string;
sender: string;
member: string;
members: [sender: string, ...member: string[]];
auth_events: string[];
prev_events: string[];
depth: number;
ts?: number;
}) => {
const [sender, ...members] = usernames;
return createEventBase("m.room.power_levels", {
roomId,
sender,
Expand All @@ -54,7 +53,10 @@ export const roomPowerLevelsEvent = ({
depth,
ts,
content: {
users: { [sender]: 100, [member]: 100 },
users: {
[sender]: 100,
...Object.fromEntries(usernames.map((member) => [member, 100])),
},
users_default: 0,
events: {
"m.room.name": 50,
Expand Down
6 changes: 2 additions & 4 deletions packages/fake/src/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ export const fakeEndpoints = new Elysia({ prefix: "/fake" })
}

const { roomId, events } = await createRoom(
sender,
username,
[sender, username],
createSignedEvent(config.signingKey[0], config.name),
`!${createMediaId(18)}:${config.name}`,
);
Expand Down Expand Up @@ -112,8 +111,7 @@ export const fakeEndpoints = new Elysia({ prefix: "/fake" })
}

const { roomId: newRoomId, events } = await createRoom(
sender,
username,
[sender, username],
createSignedEvent(config.signingKey[0], config.name),
`!${createMediaId(18)}:${config.name}`,
);
Expand Down
4 changes: 2 additions & 2 deletions packages/homeserver/src/authentication.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test";

import {
computeHash,
computeAndMergeHash,
extractSignaturesFromHeader,
generateId,
signRequest,
Expand Down Expand Up @@ -142,7 +142,7 @@ test("signRequest", async () => {
});

test("computeHash", async () => {
const result = computeHash({
const result = computeAndMergeHash({
auth_events: [
"$e0YmwnKseuHqsuF50ekjta7z5UpO-bDoq7y4R1NKMpI",
"$6_VX-xW821oaBwOuaaV_xoC6fD2iMg2QPWD4J7Bh3o4",
Expand Down
34 changes: 27 additions & 7 deletions packages/homeserver/src/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

if (Object.keys(rest).length) {
// it should never happen since the regex should match all the parameters
throw new Error("Invalid authorization header, unexpected parameters");

Check warning on line 34 in packages/homeserver/src/authentication.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

34 line is not covered with tests
}

if ([origin, destination, key, signature].some((value) => !value)) {
Expand Down Expand Up @@ -132,7 +132,7 @@
};
};

export function computeHash<T extends Record<string, unknown>>(
export function computeAndMergeHash<T extends Record<string, unknown>>(
content: T,
): HashedEvent<T> {
// remove the fields that are not part of the hash
Expand All @@ -146,19 +146,39 @@
...toHash
} = content as any;

const [algorithm, hash] = computeHash(toHash);

return {
...content,
hashes: {
sha256: toUnpaddedBase64(
crypto
.createHash("sha256")
.update(encodeCanonicalJson(toHash))
.digest(),
),
[algorithm]: hash,
},
};
}

export function computeHash<T extends Record<string, unknown>>(
content: T,
algorithm: "sha256" = "sha256",
): ["sha256", string] {
// remove the fields that are not part of the hash
const {
age_ts,
unsigned,
signatures,
hashes,
outlier,
destinations,
...toHash
} = content as any;

return [
algorithm,
toUnpaddedBase64(
crypto.createHash(algorithm).update(encodeCanonicalJson(toHash)).digest(),
),
];
}

export function generateId<T extends object>(content: T): string {
// remove the fields that are not part of the hash
const { age_ts, unsigned, signatures, ...toHash } = pruneEventDict(
Expand Down
40 changes: 40 additions & 0 deletions packages/homeserver/src/fixtures/ContextBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Elysia from "elysia";
import Crypto from "node:crypto";

import { type SigningKey, generateKeyPairsFromString } from "../keys";
import { toUnpaddedBase64 } from "../binaryData";
import type {
Expand All @@ -9,6 +11,8 @@
import type { HomeServerRoutes } from "../app";
import type { EventBase } from "@hs/core/src/events/eventBase";
import type { EventStore } from "../plugins/mongodb";
import { createRoom } from "../procedures/createRoom";
import { createSignedEvent } from "@hs/core/src/events/utils/createSignedEvent";

type MockedFakeRequest = <
M extends HomeServerRoutes["method"],
Expand All @@ -19,6 +23,28 @@
body: getAllResponsesByPath<HomeServerRoutes, M, U>["body"],
) => Promise<Request>;

export function createMediaId(length: number) {
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
let result = "";
for (let i = 0; i < length; i++) {
const randomIndex = Crypto.randomInt(0, characters.length);
result += characters[randomIndex];
}

Check warning on line 32 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

26-32 lines are not covered with tests
return result;
}

class MockedRoom {
private events: EventStore[] = [];
constructor(
private roomId: string,
events: EventStore[],
) {
for (const event of events) {
this.events.push(event);

Check warning on line 43 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

38-43 lines are not covered with tests
}
}
}

export class ContextBuilder {
private config: any;
private mongo: any;
Expand All @@ -35,33 +61,33 @@
return new ContextBuilder(name);
}

public withName(name: string) {
this.name = name;

Check warning on line 65 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

64-65 lines are not covered with tests
return this;
}

public withEvent(roomId: string, event: EventBase) {
const arr = this.events.get(roomId) || [];
arr.push({
_id: generateId(event),
event,
});
this.events.set(roomId, arr);

Check warning on line 75 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

69-75 lines are not covered with tests
return this;
}

public withConfig(config: any) {
this.config = config;

Check warning on line 80 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

79-80 lines are not covered with tests
return this;
}

public withMongo(mongo: any) {
this.mongo = mongo;

Check warning on line 85 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

84-85 lines are not covered with tests
return this;
}

public withMutex(mutex: any) {
this.mutex = mutex;

Check warning on line 90 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

89-90 lines are not covered with tests
return this;
}

Expand All @@ -75,8 +101,8 @@
return this;
}

public withRemoteSigningKey(remote: string, signingKey: SigningKey) {
this.remoteRemoteSigningKeys.set(remote, signingKey);

Check warning on line 105 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

104-105 lines are not covered with tests
return this;
}

Expand All @@ -86,6 +112,7 @@
app: Elysia<any, any, any, any, any, any>;
instance: ContextBuilder;
makeRequest: MockedFakeRequest;
createRoom: (sender: string, ...members: string[]) => Promise<MockedRoom>;
}> {
const signature = await generateKeyPairsFromString(this.signingSeed);

Expand Down Expand Up @@ -116,13 +143,13 @@
?.filter((event) => eventIds.includes(event._id)) ?? []
);
},
createStagingEvent: async (event: EventBase) => {
const id = generateId(event);
this.events.get(event.room_id)?.push({
_id: id,
event,
staged: true,
});

Check warning on line 152 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

146-152 lines are not covered with tests
return id;
},
createEvent: async (event: EventBase) => {
Expand Down Expand Up @@ -155,12 +182,25 @@
body: body && JSON.stringify(body),
});
};

return {
signature,
name: this.name,
app,
instance: this,
makeRequest,
createRoom: async (sender: string, ...members: string[]) => {
const { roomId, events } = await createRoom(
[sender, ...members],
createSignedEvent(config.signingKey[0], config.name),
`!${createMediaId(18)}:${config.name}`,
);

for (const { event } of events) {
this.withEvent(roomId, event);
}
return new MockedRoom(roomId, events);
},
};
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/homeserver/src/makeRequest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { HomeServerRoutes } from "./app";
import { authorizationHeaders, computeHash } from "./authentication";
import { authorizationHeaders, computeAndMergeHash } from "./authentication";
import { resolveHostAddressByServerName } from "./helpers/server-discovery/discovery";
import { extractURIfromURL } from "./helpers/url";
import type { SigningKey } from "./keys";
Expand Down Expand Up @@ -50,7 +50,7 @@ export const makeSignedRequest = async <
const signedBody =
body &&
(await signJson(
computeHash({ ...body, signatures: {} }),
computeAndMergeHash({ ...body, signatures: {} }),
signingKey,
signingName,
));
Expand Down
13 changes: 12 additions & 1 deletion packages/homeserver/src/mutex/Mutex.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
export class Mutex {
private map: Map<string, boolean> = new Map();
public async request(scope: string) {

public async request(scope: string, fail: true): Promise<Lock>;

public async request(scope: string): Promise<Lock | false>;
public async request(scope: string, fail?: true) {
if (this.map.has(scope)) {
if (fail) {
throw new Error("Mutex already locked");
}
return false;
}

Expand All @@ -20,4 +27,8 @@ export class Lock {
public async release() {
this.unlock();
}

[Symbol.dispose]() {
this.release();
}
}
54 changes: 54 additions & 0 deletions packages/homeserver/src/plugins/mongodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,18 @@ export interface Server {
// }[];
}

interface Room {
_id: string;
state: EventBase[];
}

export const routerWithMongodb = (db: Db) =>
new Elysia().decorate(
"mongo",
(() => {
const eventsCollection = db.collection<EventStore>("events");
const serversCollection = db.collection<Server>("servers");
const roomsCollection = db.collection<Room>("rooms");

const getLastEvent = async (roomId: string) => {
return eventsCollection.findOne(
Expand All @@ -37,6 +43,24 @@ export const routerWithMongodb = (db: Db) =>
);
};

const upsertRoom = async (roomId: string, state: EventBase[]) => {
await roomsCollection.findOneAndUpdate(
{ _id: roomId },
{
$set: {
_id: roomId,
state,
},
},
{ upsert: true },
);
};

const getEventsByIds = async (roomId: string, eventIds: string[]) => {
return eventsCollection
.find({ "event.room_id": roomId, "event._id": { $in: eventIds } })
.toArray();
};
const getDeepEarliestAndLatestEvents = async (
roomId: string,
earliest_events: string[],
Expand Down Expand Up @@ -165,6 +189,30 @@ export const routerWithMongodb = (db: Db) =>
return id;
};

const createEvent = async (event: EventBase) => {
const id = generateId(event);
await eventsCollection.insertOne({
_id: id,
event,
});

return id;
};

const removeEventFromStaged = async (roomId: string, id: string) => {
await eventsCollection.updateOne(
{ _id: id, "event.room_id": roomId },
{ $unset: { staged: 1 } },
);
};

const getOldestStagedEvent = async (roomId: string) => {
return eventsCollection.findOne(
{ staged: true, "event.room_id": roomId },
{ sort: { "event.origin_server_ts": 1 } },
);
};

return {
serversCollection,
getValidPublicKeyFromLocal,
Expand All @@ -175,7 +223,13 @@ export const routerWithMongodb = (db: Db) =>
getMissingEventsByDeep,
getLastEvent,
getAuthEvents,

removeEventFromStaged,
getEventsByIds,
getOldestStagedEvent,
createStagingEvent,
createEvent,
upsertRoom,
};
})(),
);
Expand Down
3 changes: 1 addition & 2 deletions packages/homeserver/src/procedures/createRoom.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ test("createRoom", async () => {
const makeSignedEvent = createSignedEvent(signature, "hs1");

const { roomId, events } = await createRoom(
"@sender:hs1",
"username",
["@sender:hs1", "@username:hs1"],
makeSignedEvent,
"!roomId:hs1",
);
Expand Down
8 changes: 4 additions & 4 deletions packages/homeserver/src/procedures/createRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import { createRoomHistoryVisibilityEvent } from "@hs/core/src/events/m.room.his
import { createRoomGuestAccessEvent } from "@hs/core/src/events/m.room.guest_access";

export const createRoom = async (
sender: string,
username: string,
users: [sender: string, ...username: string[]],
makeSignedEvent: ReturnType<typeof createSignedEvent>,
roomId: string,
): Promise<{
Expand All @@ -23,6 +22,8 @@ export const createRoom = async (
}> => {
// Create

const [sender, ...members] = users;

const createRoomSigned = createRoomCreateEvent(makeSignedEvent);

const createMemberRoomSigned = createRoomMemberEvent(makeSignedEvent);
Expand Down Expand Up @@ -62,8 +63,7 @@ export const createRoom = async (

const powerLevelsEvent = await createPowerLevelsRoomSigned({
roomId,
sender,
member: username,
members: [sender, ...members],
auth_events: [createEvent._id, memberEvent._id],
prev_events: [memberEvent._id],
depth: 3,
Expand Down
Loading
Loading