Merge pull request #9789 from matrix-org/rav/edited_events

Ensure that events are correctly updated when they are edited.
pull/28217/head
Richard van der Hoff 2022-12-20 14:00:06 +00:00 committed by GitHub
commit 26a01aba00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 365 additions and 81 deletions

View File

@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
import type { CypressBot } from "../../support/bot";
import { SynapseInstance } from "../../plugins/synapsedocker";
import Chainable = Cypress.Chainable;
type EmojiMapping = [emoji: string, name: string];
interface CryptoTestContext extends Mocha.Context {
synapse: SynapseInstance;
bob: MatrixClient;
bob: CypressBot;
}
const waitForVerificationRequest = (cli: MatrixClient): Promise<VerificationRequest> => {
@ -197,7 +197,7 @@ describe("Cryptography", function () {
cy.bootstrapCrossSigning();
autoJoin(this.bob);
/* we need to have a room with the other user present, so we can open the verification panel */
// we need to have a room with the other user present, so we can open the verification panel
let roomId: string;
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }).then((_room1Id) => {
roomId = _room1Id;
@ -210,4 +210,85 @@ describe("Cryptography", function () {
verify.call(this);
});
it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) {
cy.bootstrapCrossSigning();
// bob has a second, not cross-signed, device
cy.loginBot(this.synapse, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice");
autoJoin(this.bob);
// first create the room, so that we can open the verification panel
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] })
.as("testRoomId")
.then((roomId) => {
cy.log(`Created test room ${roomId}`);
cy.visit(`/#/room/${roomId}`);
// enable encryption
cy.getClient().then((cli) => {
cli.sendStateEvent(roomId, "m.room.encryption", { algorithm: "m.megolm.v1.aes-sha2" });
});
// wait for Bob to join the room, otherwise our attempt to open his user details may race
// with his join.
cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist");
});
verify.call(this);
cy.get<string>("@testRoomId").then((roomId) => {
// bob sends a valid event
cy.wrap(this.bob.sendTextMessage(roomId, "Hoo!")).as("testEvent");
// the message should appear, decrypted, with no warning
cy.contains(".mx_EventTile_body", "Hoo!")
.closest(".mx_EventTile")
.should("have.class", "mx_EventTile_verified")
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning");
// bob sends an edit to the first message with his unverified device
cy.get<MatrixClient>("@bobSecondDevice").then((bobSecondDevice) => {
cy.get<ISendEventResponse>("@testEvent").then((testEvent) => {
bobSecondDevice.sendMessage(roomId, {
"m.new_content": {
msgtype: "m.text",
body: "Haa!",
},
"m.relates_to": {
rel_type: "m.replace",
event_id: testEvent.event_id,
},
});
});
});
// the edit should have a warning
cy.contains(".mx_EventTile_body", "Haa!")
.closest(".mx_EventTile")
.within(() => {
cy.get(".mx_EventTile_e2eIcon_warning").should("exist");
});
// a second edit from the verified device should be ok
cy.get<ISendEventResponse>("@testEvent").then((testEvent) => {
this.bob.sendMessage(roomId, {
"m.new_content": {
msgtype: "m.text",
body: "Hee!",
},
"m.relates_to": {
rel_type: "m.replace",
event_id: testEvent.event_id,
},
});
});
cy.contains(".mx_EventTile_body", "Hee!")
.closest(".mx_EventTile")
.should("have.class", "mx_EventTile_verified")
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning");
});
});
});

View File

@ -51,6 +51,10 @@ const defaultCreateBotOptions = {
bootstrapCrossSigning: true,
} as CreateBotOpts;
export interface CypressBot extends MatrixClient {
__cypress_password: string;
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
@ -60,7 +64,7 @@ declare global {
* @param synapse the instance on which to register the bot user
* @param opts create bot options
*/
getBot(synapse: SynapseInstance, opts: CreateBotOpts): Chainable<MatrixClient>;
getBot(synapse: SynapseInstance, opts: CreateBotOpts): Chainable<CypressBot>;
/**
* Returns a new Bot instance logged in as an existing user
* @param synapse the instance on which to register the bot user
@ -156,13 +160,19 @@ function setupBotClient(
});
}
Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): Chainable<MatrixClient> => {
Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): Chainable<CypressBot> => {
opts = Object.assign({}, defaultCreateBotOptions, opts);
const username = Cypress._.uniqueId(opts.userIdPrefix);
const password = Cypress._.uniqueId("password_");
return cy.registerUser(synapse, username, password, opts.displayName).then((credentials) => {
return cy
.registerUser(synapse, username, password, opts.displayName)
.then((credentials) => {
cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`);
return setupBotClient(synapse, credentials, opts);
})
.then((client): Chainable<CypressBot> => {
Object.assign(client, { __cypress_password: password });
return cy.wrap(client as CypressBot);
});
});

View File

@ -232,7 +232,7 @@ interface IState {
// Whether the action bar is focused.
actionBarFocused: boolean;
// Whether the event's sender has been verified.
verified: string;
verified: string | null;
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions?: Relations | null | undefined;
@ -278,7 +278,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
this.state = {
// Whether the action bar is focused.
actionBarFocused: false,
// Whether the event's sender has been verified.
// Whether the event's sender has been verified. `null` if no attempt has yet been made to verify
// (including if the event is not encrypted).
verified: null,
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions: this.getReactions(),
@ -371,6 +372,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
this.props.mxEvent.on(MatrixEventEvent.Decrypted, this.onDecrypted);
this.props.mxEvent.on(MatrixEventEvent.Replaced, this.onReplaced);
DecryptionFailureTracker.instance.addVisibleEvent(this.props.mxEvent);
if (this.props.showReactions) {
this.props.mxEvent.on(MatrixEventEvent.RelationsCreated, this.onReactionsCreated);
@ -395,7 +397,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
const room = client.getRoom(this.props.mxEvent.getRoomId());
room?.on(ThreadEvent.New, this.onNewThread);
this.verifyEvent(this.props.mxEvent);
this.verifyEvent();
}
private get supportsThreadNotifications(): boolean {
@ -461,6 +463,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
}
this.isListeningForReceipts = false;
this.props.mxEvent.removeListener(MatrixEventEvent.Decrypted, this.onDecrypted);
this.props.mxEvent.removeListener(MatrixEventEvent.Replaced, this.onReplaced);
if (this.props.showReactions) {
this.props.mxEvent.removeListener(MatrixEventEvent.RelationsCreated, this.onReactionsCreated);
}
@ -470,7 +473,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
this.threadState?.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
}
public componentDidUpdate(prevProps: Readonly<EventTileProps>) {
public componentDidUpdate(prevProps: Readonly<EventTileProps>, prevState: Readonly<IState>) {
// If the verification state changed, the height might have changed
if (prevState.verified !== this.state.verified && this.props.onHeightChanged) {
this.props.onHeightChanged();
}
// If we're not listening for receipts and expect to be, register a listener.
if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) {
MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt);
@ -478,7 +485,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
}
// re-check the sender verification as outgoing events progress through the send process.
if (prevProps.eventSendStatus !== this.props.eventSendStatus) {
this.verifyEvent(this.props.mxEvent);
this.verifyEvent();
}
}
@ -586,26 +593,36 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
*/
private onDecrypted = () => {
// we need to re-verify the sending device.
// (we call onHeightChanged in verifyEvent to handle the case where decryption
// has caused a change in size of the event tile)
this.verifyEvent(this.props.mxEvent);
this.forceUpdate();
this.verifyEvent();
// decryption might, of course, trigger a height change, so call onHeightChanged after the re-render
this.forceUpdate(this.props.onHeightChanged);
};
private onDeviceVerificationChanged = (userId: string, device: string): void => {
if (userId === this.props.mxEvent.getSender()) {
this.verifyEvent(this.props.mxEvent);
this.verifyEvent();
}
};
private onUserVerificationChanged = (userId: string, _trustStatus: UserTrustLevel): void => {
if (userId === this.props.mxEvent.getSender()) {
this.verifyEvent(this.props.mxEvent);
this.verifyEvent();
}
};
private async verifyEvent(mxEvent: MatrixEvent): Promise<void> {
/** called when the event is edited after we show it. */
private onReplaced = () => {
// re-verify the event if it is replaced (the edit may not be verified)
this.verifyEvent();
};
private verifyEvent(): void {
// if the event was edited, show the verification info for the edit, not
// the original
const mxEvent = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent;
if (!mxEvent.isEncrypted() || mxEvent.isRedacted()) {
this.setState({ verified: null });
return;
}
@ -615,12 +632,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
if (encryptionInfo.mismatchedSender) {
// something definitely wrong is going on here
this.setState(
{
verified: E2EState.Warning,
},
this.props.onHeightChanged,
); // Decryption may have caused a change in size
this.setState({ verified: E2EState.Warning });
return;
}
@ -628,53 +640,28 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
// If the message is unauthenticated, then display a grey
// shield, otherwise if the user isn't cross-signed then
// nothing's needed
this.setState(
{
verified: encryptionInfo.authenticated ? E2EState.Normal : E2EState.Unauthenticated,
},
this.props.onHeightChanged,
); // Decryption may have caused a change in size
this.setState({ verified: encryptionInfo.authenticated ? E2EState.Normal : E2EState.Unauthenticated });
return;
}
const eventSenderTrust =
encryptionInfo.sender && MatrixClientPeg.get().checkDeviceTrust(senderId, encryptionInfo.sender.deviceId);
if (!eventSenderTrust) {
this.setState(
{
verified: E2EState.Unknown,
},
this.props.onHeightChanged,
); // Decryption may have caused a change in size
this.setState({ verified: E2EState.Unknown });
return;
}
if (!eventSenderTrust.isVerified()) {
this.setState(
{
verified: E2EState.Warning,
},
this.props.onHeightChanged,
); // Decryption may have caused a change in size
this.setState({ verified: E2EState.Warning });
return;
}
if (!encryptionInfo.authenticated) {
this.setState(
{
verified: E2EState.Unauthenticated,
},
this.props.onHeightChanged,
); // Decryption may have caused a change in size
this.setState({ verified: E2EState.Unauthenticated });
return;
}
this.setState(
{
verified: E2EState.Verified,
},
this.props.onHeightChanged,
); // Decryption may have caused a change in size
this.setState({ verified: E2EState.Verified });
}
private propsEqual(objA: EventTileProps, objB: EventTileProps): boolean {
@ -768,7 +755,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
};
private renderE2EPadlock() {
const ev = this.props.mxEvent;
// if the event was edited, show the verification info for the edit, not
// the original
const ev = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent;
// no icon for local rooms
if (isLocalRoom(ev.getRoomId()!)) return;

View File

@ -14,19 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import * as React from "react";
import { act, render, screen, waitFor } from "@testing-library/react";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { IEncryptedEventInfo } from "matrix-js-sdk/src/crypto/api";
import EventTile, { EventTileProps } from "../../../../src/components/views/rooms/EventTile";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { getRoomContext, mkEvent, mkMessage, stubClient } from "../../../test-utils";
import { getRoomContext, mkEncryptedEvent, mkEvent, mkMessage, stubClient } from "../../../test-utils";
import { mkThread } from "../../../test-utils/threads";
describe("EventTile", () => {
@ -34,6 +37,7 @@ describe("EventTile", () => {
let mxEvent: MatrixEvent;
let room: Room;
let client: MatrixClient;
// let changeEvent: (event: MatrixEvent) => void;
function TestEventTile(props: Partial<EventTileProps>) {
@ -67,7 +71,7 @@ describe("EventTile", () => {
stubClient();
client = MatrixClientPeg.get();
room = new Room(ROOM_ID, client, client.getUserId(), {
room = new Room(ROOM_ID, client, client.getUserId()!, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
@ -140,18 +144,194 @@ describe("EventTile", () => {
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0);
act(() => {
room.setThreadUnreadNotificationCount(mxEvent.getId(), NotificationCountType.Total, 3);
room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Total, 3);
});
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(0);
act(() => {
room.setThreadUnreadNotificationCount(mxEvent.getId(), NotificationCountType.Highlight, 1);
room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Highlight, 1);
});
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(1);
});
});
describe("Event verification", () => {
// data for our stubbed getEventEncryptionInfo: a map from event id to result
const eventToEncryptionInfoMap = new Map<string, IEncryptedEventInfo>();
const TRUSTED_DEVICE = DeviceInfo.fromStorage({}, "TRUSTED_DEVICE");
const UNTRUSTED_DEVICE = DeviceInfo.fromStorage({}, "UNTRUSTED_DEVICE");
beforeEach(() => {
eventToEncryptionInfoMap.clear();
// a mocked version of getEventEncryptionInfo which will pick its result from `eventToEncryptionInfoMap`
client.getEventEncryptionInfo = (event) => eventToEncryptionInfoMap.get(event.getId()!)!;
// a mocked version of checkUserTrust which always says the user is trusted (we do our testing via
// unverified devices).
const trustedUserTrustLevel = new UserTrustLevel(true, true, true);
client.checkUserTrust = (_userId) => trustedUserTrustLevel;
// a version of checkDeviceTrust which says that TRUSTED_DEVICE is trusted, and others are not.
const trustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, true, false);
const untrustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, false, false);
client.checkDeviceTrust = (userId, deviceId) => {
if (deviceId === TRUSTED_DEVICE.deviceId) {
return trustedDeviceTrustLevel;
} else {
return untrustedDeviceTrustLevel;
}
};
});
it("shows a warning for an event from an unverified device", async () => {
mxEvent = await mkEncryptedEvent({
plainContent: { msgtype: "m.text", body: "msg1" },
plainType: "m.room.message",
user: "@alice:example.org",
room: room.roomId,
});
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
authenticated: true,
sender: UNTRUSTED_DEVICE,
} as IEncryptedEventInfo);
const { container } = getComponent();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
const eventTile = eventTiles[0];
expect(eventTile.classList).toContain("mx_EventTile_unverified");
// there should be a warning shield
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
"mx_EventTile_e2eIcon_warning",
);
});
it("shows no shield for a verified event", async () => {
mxEvent = await mkEncryptedEvent({
plainContent: { msgtype: "m.text", body: "msg1" },
plainType: "m.room.message",
user: "@alice:example.org",
room: room.roomId,
});
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
authenticated: true,
sender: TRUSTED_DEVICE,
} as IEncryptedEventInfo);
const { container } = getComponent();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
const eventTile = eventTiles[0];
expect(eventTile.classList).toContain("mx_EventTile_verified");
// there should be no warning
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
});
it("should update the warning when the event is edited", async () => {
// we start out with an event from the trusted device
mxEvent = await mkEncryptedEvent({
plainContent: { msgtype: "m.text", body: "msg1" },
plainType: "m.room.message",
user: "@alice:example.org",
room: room.roomId,
});
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
authenticated: true,
sender: TRUSTED_DEVICE,
} as IEncryptedEventInfo);
const { container } = getComponent();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
const eventTile = eventTiles[0];
expect(eventTile.classList).toContain("mx_EventTile_verified");
// there should be no warning
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
// then we replace the event with one from the unverified device
const replacementEvent = await mkEncryptedEvent({
plainContent: { msgtype: "m.text", body: "msg1" },
plainType: "m.room.message",
user: "@alice:example.org",
room: room.roomId,
});
eventToEncryptionInfoMap.set(replacementEvent.getId()!, {
authenticated: true,
sender: UNTRUSTED_DEVICE,
} as IEncryptedEventInfo);
act(() => {
mxEvent.makeReplaced(replacementEvent);
});
// check it was updated
expect(eventTile.classList).toContain("mx_EventTile_unverified");
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
"mx_EventTile_e2eIcon_warning",
);
});
it("should update the warning when the event is replaced with an unencrypted one", async () => {
jest.spyOn(client, "isRoomEncrypted").mockReturnValue(true);
// we start out with an event from the trusted device
mxEvent = await mkEncryptedEvent({
plainContent: { msgtype: "m.text", body: "msg1" },
plainType: "m.room.message",
user: "@alice:example.org",
room: room.roomId,
});
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
authenticated: true,
sender: TRUSTED_DEVICE,
} as IEncryptedEventInfo);
const { container } = getComponent();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
const eventTile = eventTiles[0];
expect(eventTile.classList).toContain("mx_EventTile_verified");
// there should be no warning
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
// then we replace the event with an unencrypted one
const replacementEvent = await mkMessage({
msg: "msg2",
user: "@alice:example.org",
room: room.roomId,
event: true,
});
act(() => {
mxEvent.makeReplaced(replacementEvent);
});
// check it was updated
expect(eventTile.classList).not.toContain("mx_EventTile_verified");
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
"mx_EventTile_e2eIcon_warning",
);
});
});
});

View File

@ -38,6 +38,8 @@ import { normalize } from "matrix-js-sdk/src/utils";
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { CryptoBackend } from "matrix-js-sdk/src/common-crypto/CryptoBackend";
import { IEventDecryptionResult } from "matrix-js-sdk/src/@types/crypto";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
@ -316,26 +318,48 @@ export function mkEvent(opts: MakeEventProps): MatrixEvent {
}
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
* Create an m.room.encrypted event
*
* @param opts - Values for the event
* @param opts.room - The ID of the room for the event
* @param opts.user - The sender of the event
* @param opts.plainType - The type the event will have, once it has been decrypted
* @param opts.plainContent - The content the event will have, once it has been decrypted
*/
export function mkPresence(opts) {
if (!opts.user) {
throw new Error("Missing user");
}
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.user,
content: {
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
presence: opts.presence || "offline",
export async function mkEncryptedEvent(opts: {
room: Room["roomId"];
user: User["userId"];
plainType: string;
plainContent: IContent;
}): Promise<MatrixEvent> {
// we construct an event which has been decrypted by stubbing out CryptoBackend.decryptEvent and then
// calling MatrixEvent.attemptDecryption.
const mxEvent = mkEvent({
type: "m.room.encrypted",
room: opts.room,
user: opts.user,
event: true,
content: {},
});
const decryptionResult: IEventDecryptionResult = {
claimedEd25519Key: "",
clearEvent: {
type: opts.plainType,
content: opts.plainContent,
},
forwardingCurve25519KeyChain: [],
senderCurve25519Key: "",
untrusted: false,
};
return opts.event ? new MatrixEvent(event) : event;
const mockCrypto = {
decryptEvent: async (_ev): Promise<IEventDecryptionResult> => decryptionResult,
} as CryptoBackend;
await mxEvent.attemptDecryption(mockCrypto);
return mxEvent;
}
/**