mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:50:43 +00:00
Matrix: expose E2EE QA verification hooks
This commit is contained in:
@@ -98,6 +98,7 @@ export function resolveMatrixRoomKeyBackupIssue(
|
||||
export function resolveMatrixRoomKeyBackupReadinessError(
|
||||
backup: MatrixRoomKeyBackupStatusLike,
|
||||
opts: {
|
||||
allowUntrustedMatchingKey?: boolean;
|
||||
requireServerBackup: boolean;
|
||||
},
|
||||
): string | null {
|
||||
@@ -108,6 +109,14 @@ export function resolveMatrixRoomKeyBackupReadinessError(
|
||||
if (issue.code === "ok") {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
issue.code === "untrusted-signature" &&
|
||||
opts.allowUntrustedMatchingKey === true &&
|
||||
backup.matchesDecryptionKey === true &&
|
||||
backup.decryptionKeyCached === true
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (issue.message) {
|
||||
return `Matrix room key backup is not usable: ${issue.message}.`;
|
||||
}
|
||||
|
||||
@@ -256,11 +256,13 @@ export function createMatrixTextMessageEvent(params: {
|
||||
originServerTs?: number;
|
||||
relatesTo?: RoomMessageEventContent["m.relates_to"];
|
||||
mentions?: RoomMessageEventContent["m.mentions"];
|
||||
unsigned?: MatrixRawEvent["unsigned"];
|
||||
}): MatrixRawEvent {
|
||||
return createMatrixRoomMessageEvent({
|
||||
eventId: params.eventId,
|
||||
sender: params.sender,
|
||||
originServerTs: params.originServerTs,
|
||||
unsigned: params.unsigned,
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: params.body,
|
||||
@@ -274,6 +276,7 @@ export function createMatrixRoomMessageEvent(params: {
|
||||
eventId: string;
|
||||
sender?: string;
|
||||
originServerTs?: number;
|
||||
unsigned?: MatrixRawEvent["unsigned"];
|
||||
content: RoomMessageEventContent;
|
||||
}): MatrixRawEvent {
|
||||
return {
|
||||
@@ -282,6 +285,7 @@ export function createMatrixRoomMessageEvent(params: {
|
||||
event_id: params.eventId,
|
||||
origin_server_ts: params.originServerTs ?? Date.now(),
|
||||
content: params.content,
|
||||
...(params.unsigned ? { unsigned: params.unsigned } : {}),
|
||||
} as MatrixRawEvent;
|
||||
}
|
||||
|
||||
|
||||
@@ -564,6 +564,32 @@ describe("matrix monitor handler pairing account scope", () => {
|
||||
expect(resolveAgentRoute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("drops root events that carry a bundled replacement relation", async () => {
|
||||
const { handler, recordInboundSession } = createMatrixHandlerTestHarness({
|
||||
isDirectMessage: false,
|
||||
mentionRegexes: [/@bot/i],
|
||||
getMemberDisplayName: async () => "sender",
|
||||
});
|
||||
|
||||
await handler(
|
||||
"!room:example.org",
|
||||
createMatrixTextMessageEvent({
|
||||
eventId: "$edited-root",
|
||||
body: "@bot please reply",
|
||||
mentions: { user_ids: ["@bot:example.org"] },
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
"m.replace": {
|
||||
event_id: "$edit",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(recordInboundSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips media downloads for unmentioned group media messages", async () => {
|
||||
const downloadContent = vi.fn(async () => Buffer.from("image"));
|
||||
const getMemberDisplayName = vi.fn(async () => "sender");
|
||||
|
||||
@@ -169,6 +169,14 @@ function resolveMatrixMentionPrecheckText(params: {
|
||||
return "";
|
||||
}
|
||||
|
||||
function hasBundledMatrixReplacementRelation(event: MatrixRawEvent) {
|
||||
const relations = event.unsigned?.["m.relations"];
|
||||
if (!relations || typeof relations !== "object") {
|
||||
return false;
|
||||
}
|
||||
return relations[RelationType.Replace] !== undefined;
|
||||
}
|
||||
|
||||
function resolveMatrixInboundBodyText(params: {
|
||||
rawBody: string;
|
||||
filename?: string;
|
||||
@@ -500,6 +508,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
if (relates && "rel_type" in relates && relates.rel_type === RelationType.Replace) {
|
||||
return undefined;
|
||||
}
|
||||
if (hasBundledMatrixReplacementRelation(event)) {
|
||||
return undefined;
|
||||
}
|
||||
if (eventId && inboundDeduper) {
|
||||
claimedInboundEvent = inboundDeduper.claimEvent({ roomId, eventId });
|
||||
if (!claimedInboundEvent) {
|
||||
|
||||
@@ -1484,6 +1484,38 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
expect(status.verified).toBe(false);
|
||||
});
|
||||
|
||||
it("reports peer device trust from the current client", async () => {
|
||||
const getDeviceVerificationStatus = vi.fn(async () => ({
|
||||
isVerified: () => true,
|
||||
localVerified: true,
|
||||
crossSigningVerified: false,
|
||||
signedByOwner: false,
|
||||
}));
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
bootstrapCrossSigning: vi.fn(async () => {}),
|
||||
bootstrapSecretStorage: vi.fn(async () => {}),
|
||||
requestOwnUserVerification: vi.fn(async () => null),
|
||||
getDeviceVerificationStatus,
|
||||
}));
|
||||
|
||||
const client = new MatrixClient("https://matrix.example.org", "token", {
|
||||
encryption: true,
|
||||
});
|
||||
await client.start();
|
||||
|
||||
const status = await client.getDeviceVerificationStatus("@peer:example.org", "PEERDEVICE");
|
||||
expect(getDeviceVerificationStatus).toHaveBeenCalledWith("@peer:example.org", "PEERDEVICE");
|
||||
expect(status).toMatchObject({
|
||||
deviceId: "PEERDEVICE",
|
||||
encryptionEnabled: true,
|
||||
localVerified: true,
|
||||
signedByOwner: false,
|
||||
userId: "@peer:example.org",
|
||||
verified: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("verifies with a provided recovery key and reports success", async () => {
|
||||
const encoded = encodeRecoveryKey(new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1)));
|
||||
expect(encoded).toBeTypeOf("string");
|
||||
@@ -1885,6 +1917,38 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
expect(restoreKeyBackup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("restores backup keys when the matching decryption key is cached but signature trust is stale", async () => {
|
||||
const restoreKeyBackup = vi.fn(async () => ({ imported: 3, total: 3 }));
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
getActiveSessionBackupVersion: vi.fn(async () => "42"),
|
||||
getSessionBackupPrivateKey: vi.fn(async () => new Uint8Array([1])),
|
||||
getKeyBackupInfo: vi.fn(async () => ({
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
auth_data: {},
|
||||
version: "42",
|
||||
})),
|
||||
isKeyBackupTrusted: vi.fn(async () => ({
|
||||
trusted: false,
|
||||
matchesDecryptionKey: true,
|
||||
})),
|
||||
restoreKeyBackup,
|
||||
}));
|
||||
|
||||
const client = new MatrixClient("https://matrix.example.org", "token", {
|
||||
encryption: true,
|
||||
});
|
||||
vi.spyOn(client, "doRequest").mockResolvedValue({ version: "42" });
|
||||
|
||||
const result = await client.restoreRoomKeyBackup();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.imported).toBe(3);
|
||||
expect(result.total).toBe(3);
|
||||
expect(result.backup.trusted).toBe(false);
|
||||
expect(result.backup.matchesDecryptionKey).toBe(true);
|
||||
expect(restoreKeyBackup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("activates backup after loading the key from secret storage before restore", async () => {
|
||||
const getActiveSessionBackupVersion = vi
|
||||
.fn()
|
||||
|
||||
@@ -83,6 +83,16 @@ export type MatrixOwnDeviceVerificationStatus = {
|
||||
backup: MatrixRoomKeyBackupStatus;
|
||||
};
|
||||
|
||||
export type MatrixDeviceVerificationStatus = {
|
||||
encryptionEnabled: boolean;
|
||||
userId: string | null;
|
||||
deviceId: string | null;
|
||||
verified: boolean;
|
||||
localVerified: boolean;
|
||||
crossSigningVerified: boolean;
|
||||
signedByOwner: boolean;
|
||||
};
|
||||
|
||||
export type MatrixRoomKeyBackupStatus = {
|
||||
serverVersion: string | null;
|
||||
activeVersion: string | null;
|
||||
@@ -332,7 +342,7 @@ export class MatrixClient {
|
||||
const runtime = await loadMatrixCryptoRuntime();
|
||||
this.decryptBridge ??= new runtime.MatrixDecryptBridge<MatrixRawEvent>({
|
||||
client: this.client,
|
||||
toRaw: (event) => matrixEventToRaw(event),
|
||||
toRaw: (event) => matrixEventToRaw(event, { contentMode: "original" }),
|
||||
emitDecryptedEvent: (roomId, event) => {
|
||||
this.emitter.emit("room.decrypted_event", roomId, event);
|
||||
},
|
||||
@@ -669,6 +679,7 @@ export class MatrixClient {
|
||||
databasePrefix: this.cryptoDatabasePrefix,
|
||||
}).catch(noop);
|
||||
}, MATRIX_IDB_PERSIST_INTERVAL_MS);
|
||||
this.idbPersistTimer.unref?.();
|
||||
} catch (err) {
|
||||
LogService.warn("MatrixClientLite", "Failed to initialize rust crypto:", err);
|
||||
}
|
||||
@@ -1044,44 +1055,61 @@ export class MatrixClient {
|
||||
};
|
||||
}
|
||||
|
||||
async getOwnDeviceVerificationStatus(): Promise<MatrixOwnDeviceVerificationStatus> {
|
||||
const recoveryKey = this.recoveryKeyStore.getRecoveryKeySummary();
|
||||
const userId = this.client.getUserId() ?? this.selfUserId ?? null;
|
||||
const deviceId = this.client.getDeviceId()?.trim() || null;
|
||||
const backup = await this.getRoomKeyBackupStatus();
|
||||
|
||||
async getDeviceVerificationStatus(
|
||||
userId: string | null | undefined,
|
||||
deviceId: string | null | undefined,
|
||||
): Promise<MatrixDeviceVerificationStatus> {
|
||||
const normalizedUserId = userId?.trim() || null;
|
||||
const normalizedDeviceId = deviceId?.trim() || null;
|
||||
if (!this.encryptionEnabled) {
|
||||
return {
|
||||
encryptionEnabled: false,
|
||||
userId,
|
||||
deviceId,
|
||||
userId: normalizedUserId,
|
||||
deviceId: normalizedDeviceId,
|
||||
verified: false,
|
||||
localVerified: false,
|
||||
crossSigningVerified: false,
|
||||
signedByOwner: false,
|
||||
recoveryKeyStored: Boolean(recoveryKey),
|
||||
recoveryKeyCreatedAt: recoveryKey?.createdAt ?? null,
|
||||
recoveryKeyId: recoveryKey?.keyId ?? null,
|
||||
backupVersion: backup.serverVersion,
|
||||
backup,
|
||||
};
|
||||
}
|
||||
|
||||
const crypto = this.client.getCrypto() as MatrixCryptoBootstrapApi | undefined;
|
||||
let deviceStatus: MatrixDeviceVerificationStatusLike | null = null;
|
||||
if (crypto && userId && deviceId && typeof crypto.getDeviceVerificationStatus === "function") {
|
||||
deviceStatus = await crypto.getDeviceVerificationStatus(userId, deviceId).catch(() => null);
|
||||
if (
|
||||
crypto &&
|
||||
normalizedUserId &&
|
||||
normalizedDeviceId &&
|
||||
typeof crypto.getDeviceVerificationStatus === "function"
|
||||
) {
|
||||
deviceStatus = await crypto
|
||||
.getDeviceVerificationStatus(normalizedUserId, normalizedDeviceId)
|
||||
.catch(() => null);
|
||||
}
|
||||
const { isMatrixDeviceOwnerVerified } = await loadMatrixCryptoRuntime();
|
||||
const { isMatrixDeviceVerifiedInCurrentClient } = await loadMatrixCryptoRuntime();
|
||||
|
||||
return {
|
||||
encryptionEnabled: true,
|
||||
userId,
|
||||
deviceId,
|
||||
verified: isMatrixDeviceOwnerVerified(deviceStatus),
|
||||
userId: normalizedUserId,
|
||||
deviceId: normalizedDeviceId,
|
||||
verified: isMatrixDeviceVerifiedInCurrentClient(deviceStatus),
|
||||
localVerified: deviceStatus?.localVerified === true,
|
||||
crossSigningVerified: deviceStatus?.crossSigningVerified === true,
|
||||
signedByOwner: deviceStatus?.signedByOwner === true,
|
||||
};
|
||||
}
|
||||
|
||||
async getOwnDeviceVerificationStatus(): Promise<MatrixOwnDeviceVerificationStatus> {
|
||||
const recoveryKey = this.recoveryKeyStore.getRecoveryKeySummary();
|
||||
const userId = this.client.getUserId() ?? this.selfUserId ?? null;
|
||||
const deviceId = this.client.getDeviceId()?.trim() || null;
|
||||
const backup = await this.getRoomKeyBackupStatus();
|
||||
const deviceVerification = await this.getDeviceVerificationStatus(userId, deviceId);
|
||||
const ownerVerified =
|
||||
deviceVerification.crossSigningVerified || deviceVerification.signedByOwner;
|
||||
|
||||
return {
|
||||
...deviceVerification,
|
||||
verified: ownerVerified,
|
||||
recoveryKeyStored: Boolean(recoveryKey),
|
||||
recoveryKeyCreatedAt: recoveryKey?.createdAt ?? null,
|
||||
recoveryKeyId: recoveryKey?.keyId ?? null,
|
||||
@@ -1211,6 +1239,7 @@ export class MatrixClient {
|
||||
const backup = await this.getRoomKeyBackupStatus();
|
||||
loadedFromSecretStorage = backup.keyLoadAttempted && !backup.keyLoadError;
|
||||
const backupError = resolveMatrixRoomKeyBackupReadinessError(backup, {
|
||||
allowUntrustedMatchingKey: true,
|
||||
requireServerBackup: true,
|
||||
});
|
||||
if (backupError) {
|
||||
@@ -1668,7 +1697,7 @@ export class MatrixClient {
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = matrixEventToRaw(event);
|
||||
const raw = matrixEventToRaw(event, { contentMode: "original" });
|
||||
const isEncryptedEvent = raw.type === "m.room.encrypted";
|
||||
this.emitter.emit("room.event", roomId, raw);
|
||||
if (isEncryptedEvent) {
|
||||
|
||||
@@ -320,6 +320,84 @@ describe("MatrixCryptoBootstrapper", () => {
|
||||
expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("recreates secret storage and retries a forced reset when stale server SSSS blocks it", async () => {
|
||||
const bootstrapCrossSigning = vi
|
||||
.fn<() => Promise<void>>()
|
||||
.mockRejectedValueOnce(new Error("getSecretStorageKey callback returned falsey"))
|
||||
.mockResolvedValueOnce(undefined);
|
||||
const { deps, crypto, bootstrapper } = createBootstrapperHarness({
|
||||
bootstrapCrossSigning,
|
||||
isCrossSigningReady: vi.fn(async () => true),
|
||||
userHasCrossSigningKeys: vi.fn(async () => true),
|
||||
getDeviceVerificationStatus: vi.fn(async () => createVerifiedDeviceStatus()),
|
||||
});
|
||||
|
||||
await bootstrapper.bootstrap(crypto, {
|
||||
strict: true,
|
||||
forceResetCrossSigning: true,
|
||||
allowSecretStorageRecreateWithoutRecoveryKey: true,
|
||||
});
|
||||
|
||||
expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).toHaveBeenCalledWith(
|
||||
crypto,
|
||||
{
|
||||
allowSecretStorageRecreateWithoutRecoveryKey: true,
|
||||
forceNewSecretStorage: true,
|
||||
},
|
||||
);
|
||||
expect(bootstrapCrossSigning).toHaveBeenCalledTimes(3);
|
||||
expect(bootstrapCrossSigning).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
setupNewCrossSigning: true,
|
||||
authUploadDeviceSigningKeys: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
expect(bootstrapCrossSigning).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
authUploadDeviceSigningKeys: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("re-exports cross-signing keys after forced reset creates secret storage", async () => {
|
||||
const bootstrapCrossSigning = vi.fn(async () => {});
|
||||
const { deps, crypto, bootstrapper } = createBootstrapperHarness({
|
||||
bootstrapCrossSigning,
|
||||
isCrossSigningReady: vi.fn(async () => true),
|
||||
userHasCrossSigningKeys: vi.fn(async () => true),
|
||||
getDeviceVerificationStatus: vi.fn(async () => createVerifiedDeviceStatus()),
|
||||
});
|
||||
|
||||
await bootstrapper.bootstrap(crypto, {
|
||||
strict: true,
|
||||
forceResetCrossSigning: true,
|
||||
allowSecretStorageRecreateWithoutRecoveryKey: true,
|
||||
});
|
||||
|
||||
expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).toHaveBeenCalledWith(
|
||||
crypto,
|
||||
{
|
||||
allowSecretStorageRecreateWithoutRecoveryKey: true,
|
||||
},
|
||||
);
|
||||
expect(bootstrapCrossSigning).toHaveBeenCalledTimes(2);
|
||||
expect(bootstrapCrossSigning).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
setupNewCrossSigning: true,
|
||||
authUploadDeviceSigningKeys: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
expect(bootstrapCrossSigning).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
authUploadDeviceSigningKeys: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("fails in strict mode when cross-signing keys are still unpublished", async () => {
|
||||
const deps = createBootstrapperDeps();
|
||||
const crypto = createCryptoApi({
|
||||
|
||||
@@ -59,7 +59,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
options.allowSecretStorageRecreateWithoutRecoveryKey === true,
|
||||
});
|
||||
}
|
||||
const crossSigning = await this.bootstrapCrossSigning(crypto, {
|
||||
let crossSigning = await this.bootstrapCrossSigning(crypto, {
|
||||
forceResetCrossSigning: options.forceResetCrossSigning === true,
|
||||
allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
|
||||
allowSecretStorageRecreateWithoutRecoveryKey:
|
||||
@@ -74,6 +74,15 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
allowSecretStorageRecreateWithoutRecoveryKey:
|
||||
options.allowSecretStorageRecreateWithoutRecoveryKey === true,
|
||||
});
|
||||
if (deferSecretStorageBootstrapUntilAfterCrossSigning) {
|
||||
crossSigning = await this.bootstrapCrossSigning(crypto, {
|
||||
forceResetCrossSigning: false,
|
||||
allowAutomaticCrossSigningReset: false,
|
||||
allowSecretStorageRecreateWithoutRecoveryKey:
|
||||
options.allowSecretStorageRecreateWithoutRecoveryKey === true,
|
||||
strict,
|
||||
});
|
||||
}
|
||||
const ownDeviceVerified = await this.ensureOwnDeviceTrust(crypto, strict);
|
||||
return {
|
||||
crossSigningReady: crossSigning.ready,
|
||||
@@ -160,12 +169,38 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
};
|
||||
|
||||
if (options.forceResetCrossSigning) {
|
||||
try {
|
||||
const resetCrossSigning = async (): Promise<void> => {
|
||||
await crypto.bootstrapCrossSigning({
|
||||
setupNewCrossSigning: true,
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
};
|
||||
try {
|
||||
await resetCrossSigning();
|
||||
} catch (err) {
|
||||
const shouldRepairSecretStorage =
|
||||
options.allowSecretStorageRecreateWithoutRecoveryKey &&
|
||||
isRepairableSecretStorageAccessError(err);
|
||||
if (shouldRepairSecretStorage) {
|
||||
LogService.warn(
|
||||
"MatrixClientLite",
|
||||
"Forced cross-signing reset could not unlock secret storage; recreating secret storage and retrying.",
|
||||
);
|
||||
try {
|
||||
await this.deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey(crypto, {
|
||||
allowSecretStorageRecreateWithoutRecoveryKey: true,
|
||||
forceNewSecretStorage: true,
|
||||
});
|
||||
await resetCrossSigning();
|
||||
} catch (repairErr) {
|
||||
LogService.warn("MatrixClientLite", "Forced cross-signing reset failed:", repairErr);
|
||||
if (options.strict) {
|
||||
throw repairErr instanceof Error ? repairErr : new Error(String(repairErr));
|
||||
}
|
||||
return { ready: false, published: false };
|
||||
}
|
||||
return await finalize();
|
||||
}
|
||||
LogService.warn("MatrixClientLite", "Forced cross-signing reset failed:", err);
|
||||
if (options.strict) {
|
||||
throw err instanceof Error ? err : new Error(String(err));
|
||||
|
||||
@@ -8,4 +8,7 @@ export { MatrixDecryptBridge } from "./decrypt-bridge.js";
|
||||
export { persistIdbToDisk, restoreIdbFromDisk } from "./idb-persistence.js";
|
||||
export { MatrixVerificationManager } from "./verification-manager.js";
|
||||
export type { MatrixVerificationSummary } from "./verification-manager.js";
|
||||
export { isMatrixDeviceOwnerVerified } from "./verification-status.js";
|
||||
export {
|
||||
isMatrixDeviceOwnerVerified,
|
||||
isMatrixDeviceVerifiedInCurrentClient,
|
||||
} from "./verification-status.js";
|
||||
|
||||
@@ -57,4 +57,61 @@ describe("event-helpers", () => {
|
||||
} as unknown as MatrixEvent;
|
||||
expect(matrixEventToRaw(viaRaw).state_key).toBe("@carol:example.org");
|
||||
});
|
||||
|
||||
it("serializes current content by default for read APIs", () => {
|
||||
const event = {
|
||||
getId: () => "$root",
|
||||
getSender: () => "@alice:example.org",
|
||||
getType: () => "m.room.message",
|
||||
getTs: () => 1000,
|
||||
getOriginalContent: () => ({ body: "original", msgtype: "m.text" }),
|
||||
getContent: () => ({
|
||||
body: "@bot edited",
|
||||
"m.mentions": { user_ids: ["@bot:example.org"] },
|
||||
msgtype: "m.text",
|
||||
}),
|
||||
getUnsigned: () => ({
|
||||
"m.relations": {
|
||||
"m.replace": { event_id: "$edit" },
|
||||
},
|
||||
}),
|
||||
} as unknown as MatrixEvent;
|
||||
|
||||
expect(matrixEventToRaw(event)).toMatchObject({
|
||||
content: {
|
||||
body: "@bot edited",
|
||||
"m.mentions": { user_ids: ["@bot:example.org"] },
|
||||
msgtype: "m.text",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("can serialize original content for inbound trigger filtering", () => {
|
||||
const event = {
|
||||
getId: () => "$root",
|
||||
getSender: () => "@alice:example.org",
|
||||
getType: () => "m.room.message",
|
||||
getTs: () => 1000,
|
||||
getOriginalContent: () => ({ body: "original", msgtype: "m.text" }),
|
||||
getContent: () => ({
|
||||
body: "@bot edited",
|
||||
"m.mentions": { user_ids: ["@bot:example.org"] },
|
||||
msgtype: "m.text",
|
||||
}),
|
||||
getUnsigned: () => ({
|
||||
"m.relations": {
|
||||
"m.replace": { event_id: "$edit" },
|
||||
},
|
||||
}),
|
||||
} as unknown as MatrixEvent;
|
||||
|
||||
expect(matrixEventToRaw(event, { contentMode: "original" })).toMatchObject({
|
||||
content: { body: "original", msgtype: "m.text" },
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
"m.replace": { event_id: "$edit" },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import type { MatrixEvent } from "matrix-js-sdk";
|
||||
import type { MatrixRawEvent } from "./types.js";
|
||||
|
||||
export function matrixEventToRaw(event: MatrixEvent): MatrixRawEvent {
|
||||
export type MatrixEventContentMode = "current" | "original";
|
||||
|
||||
export function matrixEventToRaw(
|
||||
event: MatrixEvent,
|
||||
opts: { contentMode?: MatrixEventContentMode } = {},
|
||||
): MatrixRawEvent {
|
||||
const unsigned = (event.getUnsigned?.() ?? {}) as {
|
||||
age?: number;
|
||||
redacted_because?: unknown;
|
||||
};
|
||||
const eventWithOriginalContent = event as {
|
||||
getOriginalContent?: () => Record<string, unknown>;
|
||||
};
|
||||
const content =
|
||||
opts.contentMode === "original"
|
||||
? (eventWithOriginalContent.getOriginalContent?.() ?? event.getContent?.() ?? {})
|
||||
: (event.getContent?.() ?? eventWithOriginalContent.getOriginalContent?.() ?? {});
|
||||
const raw: MatrixRawEvent = {
|
||||
event_id: event.getId() ?? "",
|
||||
sender: event.getSender() ?? "",
|
||||
type: event.getType() ?? "",
|
||||
origin_server_ts: event.getTs() ?? 0,
|
||||
content: (event.getContent?.() ?? {}) || {},
|
||||
content: content || {},
|
||||
unsigned,
|
||||
};
|
||||
const stateKey = resolveMatrixStateKey(event);
|
||||
|
||||
@@ -12,6 +12,7 @@ export type MatrixRawEvent = {
|
||||
content: Record<string, unknown>;
|
||||
unsigned?: {
|
||||
age?: number;
|
||||
"m.relations"?: Record<string, unknown>;
|
||||
redacted_because?: unknown;
|
||||
};
|
||||
state_key?: string;
|
||||
|
||||
@@ -1,2 +1,21 @@
|
||||
export { matrixPlugin } from "./src/channel.js";
|
||||
export { MatrixClient } from "./src/matrix/sdk.js";
|
||||
export type {
|
||||
EncryptedFile,
|
||||
MatrixDeviceVerificationStatus,
|
||||
MatrixOwnDeviceDeleteResult,
|
||||
MatrixOwnDeviceInfo,
|
||||
MatrixOwnDeviceVerificationStatus,
|
||||
MatrixRecoveryKeyVerificationResult,
|
||||
MatrixRawEvent,
|
||||
MatrixRoomKeyBackupResetResult,
|
||||
MatrixRoomKeyBackupRestoreResult,
|
||||
MatrixRoomKeyBackupStatus,
|
||||
MatrixVerificationBootstrapResult,
|
||||
MessageEventContent,
|
||||
} from "./src/matrix/sdk.js";
|
||||
export type {
|
||||
MatrixVerificationMethod,
|
||||
MatrixVerificationSummary,
|
||||
} from "./src/matrix/sdk/verification-manager.js";
|
||||
export { setMatrixRuntime } from "./src/runtime.js";
|
||||
|
||||
Reference in New Issue
Block a user