Skip to content

Commit b2a10e6

Browse files
author
Kerry
authored
Support MSC3391: Account data deletion (#2967)
* add deleteAccountData endpoint * check server support and test * test current state of memorystore * interpret account data events with empty content as deleted * add handling for (future) stable version of endpoint * add getSafeUserId * user getSafeUserId in deleteAccountData * better jsdoc for throws documentation
1 parent 193c385 commit b2a10e6

File tree

5 files changed

+190
-1
lines changed

5 files changed

+190
-1
lines changed

spec/unit/matrix-client.spec.ts

+82
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
import { IOlmDevice } from "../../src/crypto/algorithms/megolm";
5858
import { QueryDict } from "../../src/utils";
5959
import { SyncState } from "../../src/sync";
60+
import * as featureUtils from "../../src/feature";
6061

6162
jest.useFakeTimers();
6263

@@ -281,6 +282,23 @@ describe("MatrixClient", function () {
281282
client.stopClient();
282283
});
283284

285+
describe("getSafeUserId()", () => {
286+
it("returns the logged in user id", () => {
287+
expect(client.getSafeUserId()).toEqual(userId);
288+
});
289+
290+
it("throws when there is not logged in user", () => {
291+
const notLoggedInClient = new MatrixClient({
292+
baseUrl: "https://my.home.server",
293+
idBaseUrl: identityServerUrl,
294+
fetchFn: function () {} as any, // NOP
295+
store: store,
296+
scheduler: scheduler,
297+
});
298+
expect(() => notLoggedInClient.getSafeUserId()).toThrow("Expected logged in user but found none.");
299+
});
300+
});
301+
284302
describe("sendEvent", () => {
285303
const roomId = "!room:example.org";
286304
const body = "This is the body";
@@ -1828,4 +1846,68 @@ describe("MatrixClient", function () {
18281846
expect(client.getUseE2eForGroupCall()).toBe(false);
18291847
});
18301848
});
1849+
1850+
describe("delete account data", () => {
1851+
afterEach(() => {
1852+
jest.spyOn(featureUtils, "buildFeatureSupportMap").mockRestore();
1853+
});
1854+
it("makes correct request when deletion is supported by server in unstable versions", async () => {
1855+
const eventType = "im.vector.test";
1856+
const versionsResponse = {
1857+
versions: ["1"],
1858+
unstable_features: {
1859+
"org.matrix.msc3391": true,
1860+
},
1861+
};
1862+
jest.spyOn(client.http, "request").mockResolvedValue(versionsResponse);
1863+
const requestSpy = jest.spyOn(client.http, "authedRequest").mockImplementation(() => Promise.resolve());
1864+
const unstablePrefix = "/_matrix/client/unstable/org.matrix.msc3391";
1865+
const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`;
1866+
1867+
// populate version support
1868+
await client.getVersions();
1869+
await client.deleteAccountData(eventType);
1870+
1871+
expect(requestSpy).toHaveBeenCalledWith(Method.Delete, path, undefined, undefined, {
1872+
prefix: unstablePrefix,
1873+
});
1874+
});
1875+
1876+
it("makes correct request when deletion is supported by server based on matrix version", async () => {
1877+
const eventType = "im.vector.test";
1878+
// we don't have a stable version for account data deletion yet to test this code path with
1879+
// so mock the support map to fake stable support
1880+
const stableSupportedDeletionMap = new Map();
1881+
stableSupportedDeletionMap.set(featureUtils.Feature.AccountDataDeletion, featureUtils.ServerSupport.Stable);
1882+
jest.spyOn(featureUtils, "buildFeatureSupportMap").mockResolvedValue(new Map());
1883+
const requestSpy = jest.spyOn(client.http, "authedRequest").mockImplementation(() => Promise.resolve());
1884+
const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`;
1885+
1886+
// populate version support
1887+
await client.getVersions();
1888+
await client.deleteAccountData(eventType);
1889+
1890+
expect(requestSpy).toHaveBeenCalledWith(Method.Delete, path, undefined, undefined, undefined);
1891+
});
1892+
1893+
it("makes correct request when deletion is not supported by server", async () => {
1894+
const eventType = "im.vector.test";
1895+
const versionsResponse = {
1896+
versions: ["1"],
1897+
unstable_features: {
1898+
"org.matrix.msc3391": false,
1899+
},
1900+
};
1901+
jest.spyOn(client.http, "request").mockResolvedValue(versionsResponse);
1902+
const requestSpy = jest.spyOn(client.http, "authedRequest").mockImplementation(() => Promise.resolve());
1903+
const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`;
1904+
1905+
// populate version support
1906+
await client.getVersions();
1907+
await client.deleteAccountData(eventType);
1908+
1909+
// account data updated with empty content
1910+
expect(requestSpy).toHaveBeenCalledWith(Method.Put, path, undefined, {});
1911+
});
1912+
});
18311913
});

spec/unit/stores/memory.spec.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixEvent, MemoryStore } from "../../../src";
18+
19+
describe("MemoryStore", () => {
20+
const event1 = new MatrixEvent({ type: "event1-type", content: { test: 1 } });
21+
const event2 = new MatrixEvent({ type: "event2-type", content: { test: 1 } });
22+
const event3 = new MatrixEvent({ type: "event3-type", content: { test: 1 } });
23+
const event4 = new MatrixEvent({ type: "event4-type", content: { test: 1 } });
24+
const event4Updated = new MatrixEvent({ type: "event4-type", content: { test: 2 } });
25+
const event1Empty = new MatrixEvent({ type: "event1-type", content: {} });
26+
27+
describe("account data", () => {
28+
it("sets account data events correctly", () => {
29+
const store = new MemoryStore();
30+
store.storeAccountDataEvents([event1, event2]);
31+
expect(store.getAccountData(event1.getType())).toEqual(event1);
32+
expect(store.getAccountData(event2.getType())).toEqual(event2);
33+
});
34+
35+
it("returns undefined when no account data event exists for type", () => {
36+
const store = new MemoryStore();
37+
expect(store.getAccountData("my-event-type")).toEqual(undefined);
38+
});
39+
40+
it("updates account data events correctly", () => {
41+
const store = new MemoryStore();
42+
// init store with event1, event2
43+
store.storeAccountDataEvents([event1, event2, event4]);
44+
// remove event1, add event3
45+
store.storeAccountDataEvents([event1Empty, event3, event4Updated]);
46+
// removed
47+
expect(store.getAccountData(event1.getType())).toEqual(undefined);
48+
// not removed
49+
expect(store.getAccountData(event2.getType())).toEqual(event2);
50+
// added
51+
expect(store.getAccountData(event3.getType())).toEqual(event3);
52+
// updated
53+
expect(store.getAccountData(event4.getType())).toEqual(event4Updated);
54+
});
55+
56+
it("removes all account data from state on deleteAllData", async () => {
57+
const store = new MemoryStore();
58+
store.storeAccountDataEvents([event1, event2]);
59+
await store.deleteAllData();
60+
61+
// empty object
62+
expect(store.accountData).toEqual({});
63+
});
64+
});
65+
});

src/client.ts

+32
Original file line numberDiff line numberDiff line change
@@ -1672,6 +1672,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
16721672
return null;
16731673
}
16741674

1675+
/**
1676+
* Get the user-id of the logged-in user
1677+
*
1678+
* @returns MXID for the logged-in user
1679+
* @throws Error if not logged in
1680+
*/
1681+
public getSafeUserId(): string {
1682+
const userId = this.getUserId();
1683+
if (!userId) {
1684+
throw new Error("Expected logged in user but found none.");
1685+
}
1686+
return userId;
1687+
}
1688+
16751689
/**
16761690
* Get the domain for this client's MXID
16771691
* @returns Domain of this MXID
@@ -3766,6 +3780,24 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
37663780
}
37673781
}
37683782

3783+
public async deleteAccountData(eventType: string): Promise<void> {
3784+
const msc3391DeleteAccountDataServerSupport = this.canSupport.get(Feature.AccountDataDeletion);
3785+
// if deletion is not supported overwrite with empty content
3786+
if (msc3391DeleteAccountDataServerSupport === ServerSupport.Unsupported) {
3787+
await this.setAccountData(eventType, {});
3788+
return;
3789+
}
3790+
const path = utils.encodeUri("/user/$userId/account_data/$type", {
3791+
$userId: this.getSafeUserId(),
3792+
$type: eventType,
3793+
});
3794+
const options =
3795+
msc3391DeleteAccountDataServerSupport === ServerSupport.Unstable
3796+
? { prefix: "/_matrix/client/unstable/org.matrix.msc3391" }
3797+
: undefined;
3798+
return await this.http.authedRequest(Method.Delete, path, undefined, undefined, options);
3799+
}
3800+
37693801
/**
37703802
* Gets the users that are ignored by this client
37713803
* @returns The array of users that are ignored (empty if none)

src/feature.ts

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export enum Feature {
2626
Thread = "Thread",
2727
ThreadUnreadNotifications = "ThreadUnreadNotifications",
2828
LoginTokenRequest = "LoginTokenRequest",
29+
AccountDataDeletion = "AccountDataDeletion",
2930
}
3031

3132
type FeatureSupportCondition = {
@@ -45,6 +46,9 @@ const featureSupportResolver: Record<string, FeatureSupportCondition> = {
4546
[Feature.LoginTokenRequest]: {
4647
unstablePrefixes: ["org.matrix.msc3882"],
4748
},
49+
[Feature.AccountDataDeletion]: {
50+
unstablePrefixes: ["org.matrix.msc3391"],
51+
},
4852
};
4953

5054
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {

src/store/memory.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,13 @@ export class MemoryStore implements IStore {
286286
*/
287287
public storeAccountDataEvents(events: MatrixEvent[]): void {
288288
events.forEach((event) => {
289-
this.accountData[event.getType()] = event;
289+
// MSC3391: an event with content of {} should be interpreted as deleted
290+
const isDeleted = !Object.keys(event.getContent()).length;
291+
if (isDeleted) {
292+
delete this.accountData[event.getType()];
293+
} else {
294+
this.accountData[event.getType()] = event;
295+
}
290296
});
291297
}
292298

0 commit comments

Comments
 (0)