Skip to content

Commit c23c9df

Browse files
authored
Use new CryptoApi.encryptToDeviceMessages() to send encrypted to-device messages from widgets (#28315)
1 parent 5c45ca5 commit c23c9df

File tree

2 files changed

+106
-16
lines changed

2 files changed

+106
-16
lines changed

src/stores/widgets/StopGapWidgetDriver.ts

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -416,26 +416,54 @@ export class StopGapWidgetDriver extends WidgetDriver {
416416

417417
/**
418418
* Implements {@link WidgetDriver#sendToDevice}
419-
* Encrypted to-device events are not supported.
420419
*/
421420
public async sendToDevice(
422421
eventType: string,
423422
encrypted: boolean,
424423
contentMap: { [userId: string]: { [deviceId: string]: object } },
425424
): Promise<void> {
426-
if (encrypted) throw new Error("Encrypted to-device events are not supported");
427-
428425
const client = MatrixClientPeg.safeGet();
429-
await client.queueToDevice({
430-
eventType,
431-
batch: Object.entries(contentMap).flatMap(([userId, userContentMap]) =>
432-
Object.entries(userContentMap).map(([deviceId, content]) => ({
433-
userId,
434-
deviceId,
435-
payload: content,
436-
})),
437-
),
438-
});
426+
427+
if (encrypted) {
428+
const crypto = client.getCrypto();
429+
if (!crypto) throw new Error("E2EE not enabled");
430+
431+
// attempt to re-batch these up into a single request
432+
const invertedContentMap: { [content: string]: { userId: string; deviceId: string }[] } = {};
433+
434+
for (const userId of Object.keys(contentMap)) {
435+
const userContentMap = contentMap[userId];
436+
for (const deviceId of Object.keys(userContentMap)) {
437+
const content = userContentMap[deviceId];
438+
const stringifiedContent = JSON.stringify(content);
439+
invertedContentMap[stringifiedContent] = invertedContentMap[stringifiedContent] || [];
440+
invertedContentMap[stringifiedContent].push({ userId, deviceId });
441+
}
442+
}
443+
444+
await Promise.all(
445+
Object.entries(invertedContentMap).map(async ([stringifiedContent, recipients]) => {
446+
const batch = await crypto.encryptToDeviceMessages(
447+
eventType,
448+
recipients,
449+
JSON.parse(stringifiedContent),
450+
);
451+
452+
await client.queueToDevice(batch);
453+
}),
454+
);
455+
} else {
456+
await client.queueToDevice({
457+
eventType,
458+
batch: Object.entries(contentMap).flatMap(([userId, userContentMap]) =>
459+
Object.entries(userContentMap).map(([deviceId, content]) => ({
460+
userId,
461+
deviceId,
462+
payload: content,
463+
})),
464+
),
465+
});
466+
}
439467
}
440468

441469
private pickRooms(roomIds?: (string | Symbols.AnyRoom)[]): Room[] {

test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,72 @@ describe("StopGapWidgetDriver", () => {
201201
});
202202
});
203203

204-
it("raises an error if encrypted", async () => {
205-
await expect(driver.sendToDevice("org.example.foo", true, contentMap)).rejects.toThrow(
206-
"Encrypted to-device events are not supported",
204+
it("sends encrypted messages", async () => {
205+
const encryptToDeviceMessages = jest
206+
.fn()
207+
.mockImplementation(
208+
(eventType, recipients: { userId: string; deviceId: string }[], content: object) => ({
209+
eventType: "m.room.encrypted",
210+
batch: recipients.map(({ userId, deviceId }) => ({
211+
userId,
212+
deviceId,
213+
payload: {
214+
eventType,
215+
content,
216+
},
217+
})),
218+
}),
219+
);
220+
221+
MatrixClientPeg.safeGet().getCrypto()!.encryptToDeviceMessages = encryptToDeviceMessages;
222+
223+
await driver.sendToDevice("org.example.foo", true, {
224+
"@alice:example.org": {
225+
aliceMobile: {
226+
hello: "alice",
227+
},
228+
},
229+
"@bob:example.org": {
230+
bobDesktop: {
231+
hello: "bob",
232+
},
233+
},
234+
});
235+
236+
expect(encryptToDeviceMessages).toHaveBeenCalledWith(
237+
"org.example.foo",
238+
[{ deviceId: "aliceMobile", userId: "@alice:example.org" }],
239+
{
240+
hello: "alice",
241+
},
207242
);
243+
expect(encryptToDeviceMessages).toHaveBeenCalledWith(
244+
"org.example.foo",
245+
[{ deviceId: "bobDesktop", userId: "@bob:example.org" }],
246+
{
247+
hello: "bob",
248+
},
249+
);
250+
expect(client.queueToDevice).toHaveBeenCalledWith({
251+
eventType: "m.room.encrypted",
252+
batch: expect.arrayContaining([
253+
{
254+
deviceId: "aliceMobile",
255+
payload: { content: { hello: "alice" }, eventType: "org.example.foo" },
256+
userId: "@alice:example.org",
257+
},
258+
]),
259+
});
260+
expect(client.queueToDevice).toHaveBeenCalledWith({
261+
eventType: "m.room.encrypted",
262+
batch: expect.arrayContaining([
263+
{
264+
deviceId: "bobDesktop",
265+
payload: { content: { hello: "bob" }, eventType: "org.example.foo" },
266+
userId: "@bob:example.org",
267+
},
268+
]),
269+
});
208270
});
209271
});
210272

0 commit comments

Comments
 (0)