You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
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
This commit is contained in:
@ -57,6 +57,7 @@ import {
|
|||||||
import { IOlmDevice } from "../../src/crypto/algorithms/megolm";
|
import { IOlmDevice } from "../../src/crypto/algorithms/megolm";
|
||||||
import { QueryDict } from "../../src/utils";
|
import { QueryDict } from "../../src/utils";
|
||||||
import { SyncState } from "../../src/sync";
|
import { SyncState } from "../../src/sync";
|
||||||
|
import * as featureUtils from "../../src/feature";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
@ -281,6 +282,23 @@ describe("MatrixClient", function () {
|
|||||||
client.stopClient();
|
client.stopClient();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getSafeUserId()", () => {
|
||||||
|
it("returns the logged in user id", () => {
|
||||||
|
expect(client.getSafeUserId()).toEqual(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when there is not logged in user", () => {
|
||||||
|
const notLoggedInClient = new MatrixClient({
|
||||||
|
baseUrl: "https://my.home.server",
|
||||||
|
idBaseUrl: identityServerUrl,
|
||||||
|
fetchFn: function () {} as any, // NOP
|
||||||
|
store: store,
|
||||||
|
scheduler: scheduler,
|
||||||
|
});
|
||||||
|
expect(() => notLoggedInClient.getSafeUserId()).toThrow("Expected logged in user but found none.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("sendEvent", () => {
|
describe("sendEvent", () => {
|
||||||
const roomId = "!room:example.org";
|
const roomId = "!room:example.org";
|
||||||
const body = "This is the body";
|
const body = "This is the body";
|
||||||
@ -1828,4 +1846,68 @@ describe("MatrixClient", function () {
|
|||||||
expect(client.getUseE2eForGroupCall()).toBe(false);
|
expect(client.getUseE2eForGroupCall()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("delete account data", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.spyOn(featureUtils, "buildFeatureSupportMap").mockRestore();
|
||||||
|
});
|
||||||
|
it("makes correct request when deletion is supported by server in unstable versions", async () => {
|
||||||
|
const eventType = "im.vector.test";
|
||||||
|
const versionsResponse = {
|
||||||
|
versions: ["1"],
|
||||||
|
unstable_features: {
|
||||||
|
"org.matrix.msc3391": true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
jest.spyOn(client.http, "request").mockResolvedValue(versionsResponse);
|
||||||
|
const requestSpy = jest.spyOn(client.http, "authedRequest").mockImplementation(() => Promise.resolve());
|
||||||
|
const unstablePrefix = "/_matrix/client/unstable/org.matrix.msc3391";
|
||||||
|
const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`;
|
||||||
|
|
||||||
|
// populate version support
|
||||||
|
await client.getVersions();
|
||||||
|
await client.deleteAccountData(eventType);
|
||||||
|
|
||||||
|
expect(requestSpy).toHaveBeenCalledWith(Method.Delete, path, undefined, undefined, {
|
||||||
|
prefix: unstablePrefix,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("makes correct request when deletion is supported by server based on matrix version", async () => {
|
||||||
|
const eventType = "im.vector.test";
|
||||||
|
// we don't have a stable version for account data deletion yet to test this code path with
|
||||||
|
// so mock the support map to fake stable support
|
||||||
|
const stableSupportedDeletionMap = new Map();
|
||||||
|
stableSupportedDeletionMap.set(featureUtils.Feature.AccountDataDeletion, featureUtils.ServerSupport.Stable);
|
||||||
|
jest.spyOn(featureUtils, "buildFeatureSupportMap").mockResolvedValue(new Map());
|
||||||
|
const requestSpy = jest.spyOn(client.http, "authedRequest").mockImplementation(() => Promise.resolve());
|
||||||
|
const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`;
|
||||||
|
|
||||||
|
// populate version support
|
||||||
|
await client.getVersions();
|
||||||
|
await client.deleteAccountData(eventType);
|
||||||
|
|
||||||
|
expect(requestSpy).toHaveBeenCalledWith(Method.Delete, path, undefined, undefined, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("makes correct request when deletion is not supported by server", async () => {
|
||||||
|
const eventType = "im.vector.test";
|
||||||
|
const versionsResponse = {
|
||||||
|
versions: ["1"],
|
||||||
|
unstable_features: {
|
||||||
|
"org.matrix.msc3391": false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
jest.spyOn(client.http, "request").mockResolvedValue(versionsResponse);
|
||||||
|
const requestSpy = jest.spyOn(client.http, "authedRequest").mockImplementation(() => Promise.resolve());
|
||||||
|
const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`;
|
||||||
|
|
||||||
|
// populate version support
|
||||||
|
await client.getVersions();
|
||||||
|
await client.deleteAccountData(eventType);
|
||||||
|
|
||||||
|
// account data updated with empty content
|
||||||
|
expect(requestSpy).toHaveBeenCalledWith(Method.Put, path, undefined, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
65
spec/unit/stores/memory.spec.ts
Normal file
65
spec/unit/stores/memory.spec.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixEvent, MemoryStore } from "../../../src";
|
||||||
|
|
||||||
|
describe("MemoryStore", () => {
|
||||||
|
const event1 = new MatrixEvent({ type: "event1-type", content: { test: 1 } });
|
||||||
|
const event2 = new MatrixEvent({ type: "event2-type", content: { test: 1 } });
|
||||||
|
const event3 = new MatrixEvent({ type: "event3-type", content: { test: 1 } });
|
||||||
|
const event4 = new MatrixEvent({ type: "event4-type", content: { test: 1 } });
|
||||||
|
const event4Updated = new MatrixEvent({ type: "event4-type", content: { test: 2 } });
|
||||||
|
const event1Empty = new MatrixEvent({ type: "event1-type", content: {} });
|
||||||
|
|
||||||
|
describe("account data", () => {
|
||||||
|
it("sets account data events correctly", () => {
|
||||||
|
const store = new MemoryStore();
|
||||||
|
store.storeAccountDataEvents([event1, event2]);
|
||||||
|
expect(store.getAccountData(event1.getType())).toEqual(event1);
|
||||||
|
expect(store.getAccountData(event2.getType())).toEqual(event2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when no account data event exists for type", () => {
|
||||||
|
const store = new MemoryStore();
|
||||||
|
expect(store.getAccountData("my-event-type")).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates account data events correctly", () => {
|
||||||
|
const store = new MemoryStore();
|
||||||
|
// init store with event1, event2
|
||||||
|
store.storeAccountDataEvents([event1, event2, event4]);
|
||||||
|
// remove event1, add event3
|
||||||
|
store.storeAccountDataEvents([event1Empty, event3, event4Updated]);
|
||||||
|
// removed
|
||||||
|
expect(store.getAccountData(event1.getType())).toEqual(undefined);
|
||||||
|
// not removed
|
||||||
|
expect(store.getAccountData(event2.getType())).toEqual(event2);
|
||||||
|
// added
|
||||||
|
expect(store.getAccountData(event3.getType())).toEqual(event3);
|
||||||
|
// updated
|
||||||
|
expect(store.getAccountData(event4.getType())).toEqual(event4Updated);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes all account data from state on deleteAllData", async () => {
|
||||||
|
const store = new MemoryStore();
|
||||||
|
store.storeAccountDataEvents([event1, event2]);
|
||||||
|
await store.deleteAllData();
|
||||||
|
|
||||||
|
// empty object
|
||||||
|
expect(store.accountData).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1672,6 +1672,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user-id of the logged-in user
|
||||||
|
*
|
||||||
|
* @returns MXID for the logged-in user
|
||||||
|
* @throws Error if not logged in
|
||||||
|
*/
|
||||||
|
public getSafeUserId(): string {
|
||||||
|
const userId = this.getUserId();
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("Expected logged in user but found none.");
|
||||||
|
}
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the domain for this client's MXID
|
* Get the domain for this client's MXID
|
||||||
* @returns Domain of this MXID
|
* @returns Domain of this MXID
|
||||||
@ -3766,6 +3780,24 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async deleteAccountData(eventType: string): Promise<void> {
|
||||||
|
const msc3391DeleteAccountDataServerSupport = this.canSupport.get(Feature.AccountDataDeletion);
|
||||||
|
// if deletion is not supported overwrite with empty content
|
||||||
|
if (msc3391DeleteAccountDataServerSupport === ServerSupport.Unsupported) {
|
||||||
|
await this.setAccountData(eventType, {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const path = utils.encodeUri("/user/$userId/account_data/$type", {
|
||||||
|
$userId: this.getSafeUserId(),
|
||||||
|
$type: eventType,
|
||||||
|
});
|
||||||
|
const options =
|
||||||
|
msc3391DeleteAccountDataServerSupport === ServerSupport.Unstable
|
||||||
|
? { prefix: "/_matrix/client/unstable/org.matrix.msc3391" }
|
||||||
|
: undefined;
|
||||||
|
return await this.http.authedRequest(Method.Delete, path, undefined, undefined, options);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the users that are ignored by this client
|
* Gets the users that are ignored by this client
|
||||||
* @returns The array of users that are ignored (empty if none)
|
* @returns The array of users that are ignored (empty if none)
|
||||||
|
@ -26,6 +26,7 @@ export enum Feature {
|
|||||||
Thread = "Thread",
|
Thread = "Thread",
|
||||||
ThreadUnreadNotifications = "ThreadUnreadNotifications",
|
ThreadUnreadNotifications = "ThreadUnreadNotifications",
|
||||||
LoginTokenRequest = "LoginTokenRequest",
|
LoginTokenRequest = "LoginTokenRequest",
|
||||||
|
AccountDataDeletion = "AccountDataDeletion",
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeatureSupportCondition = {
|
type FeatureSupportCondition = {
|
||||||
@ -45,6 +46,9 @@ const featureSupportResolver: Record<string, FeatureSupportCondition> = {
|
|||||||
[Feature.LoginTokenRequest]: {
|
[Feature.LoginTokenRequest]: {
|
||||||
unstablePrefixes: ["org.matrix.msc3882"],
|
unstablePrefixes: ["org.matrix.msc3882"],
|
||||||
},
|
},
|
||||||
|
[Feature.AccountDataDeletion]: {
|
||||||
|
unstablePrefixes: ["org.matrix.msc3391"],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
|
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
|
||||||
|
@ -286,7 +286,13 @@ export class MemoryStore implements IStore {
|
|||||||
*/
|
*/
|
||||||
public storeAccountDataEvents(events: MatrixEvent[]): void {
|
public storeAccountDataEvents(events: MatrixEvent[]): void {
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
this.accountData[event.getType()] = event;
|
// MSC3391: an event with content of {} should be interpreted as deleted
|
||||||
|
const isDeleted = !Object.keys(event.getContent()).length;
|
||||||
|
if (isDeleted) {
|
||||||
|
delete this.accountData[event.getType()];
|
||||||
|
} else {
|
||||||
|
this.accountData[event.getType()] = event;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user