diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 04129ab0818..52d6a07778c 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -1029,6 +1029,124 @@ describe("MatrixClient", function () { }); }); + describe("extended profiles", () => { + const unstableMSC4133Prefix = `${ClientPrefix.Unstable}/uk.tcpip.msc4133`; + const userId = "@profile_user:example.org"; + + beforeEach(() => { + unstableFeatures["uk.tcpip.msc4133"] = true; + }); + + it("throws when unsupported by server", async () => { + unstableFeatures["uk.tcpip.msc4133"] = false; + const errorMessage = "Server does not support extended profiles"; + + await expect(client.doesServerSupportExtendedProfiles()).resolves.toEqual(false); + + await expect(client.getExtendedProfile(userId)).rejects.toThrow(errorMessage); + await expect(client.getExtendedProfileProperty(userId, "test_key")).rejects.toThrow(errorMessage); + await expect(client.setExtendedProfileProperty("test_key", "foo")).rejects.toThrow(errorMessage); + await expect(client.deleteExtendedProfileProperty("test_key")).rejects.toThrow(errorMessage); + await expect(client.patchExtendedProfile({ test_key: "foo" })).rejects.toThrow(errorMessage); + await expect(client.setExtendedProfile({ test_key: "foo" })).rejects.toThrow(errorMessage); + }); + + it("can fetch a extended user profile", async () => { + const testProfile = { + test_key: "foo", + }; + httpLookups = [ + { + method: "GET", + prefix: unstableMSC4133Prefix, + path: "/profile/" + encodeURIComponent(userId), + data: testProfile, + }, + ]; + await expect(client.getExtendedProfile(userId)).resolves.toEqual(testProfile); + expect(httpLookups).toHaveLength(0); + }); + + it("can fetch a property from a extended user profile", async () => { + const testProfile = { + test_key: "foo", + }; + httpLookups = [ + { + method: "GET", + prefix: unstableMSC4133Prefix, + path: "/profile/" + encodeURIComponent(userId) + "/test_key", + data: testProfile, + }, + ]; + await expect(client.getExtendedProfileProperty(userId, "test_key")).resolves.toEqual("foo"); + expect(httpLookups).toHaveLength(0); + }); + + it("can set a property in our extended profile", async () => { + httpLookups = [ + { + method: "PUT", + prefix: unstableMSC4133Prefix, + path: "/profile/" + encodeURIComponent(client.credentials.userId!) + "/test_key", + expectBody: { + test_key: "foo", + }, + }, + ]; + await expect(client.setExtendedProfileProperty("test_key", "foo")).resolves.toEqual(undefined); + expect(httpLookups).toHaveLength(0); + }); + + it("can delete a property in our extended profile", async () => { + httpLookups = [ + { + method: "DELETE", + prefix: unstableMSC4133Prefix, + path: "/profile/" + encodeURIComponent(client.credentials.userId!) + "/test_key", + }, + ]; + await expect(client.deleteExtendedProfileProperty("test_key")).resolves.toEqual(undefined); + expect(httpLookups).toHaveLength(0); + }); + + it("can patch our extended profile", async () => { + const testProfile = { + test_key: "foo", + }; + const patchedProfile = { + existing: "key", + test_key: "foo", + }; + httpLookups = [ + { + method: "PATCH", + prefix: unstableMSC4133Prefix, + path: "/profile/" + encodeURIComponent(client.credentials.userId!), + data: patchedProfile, + expectBody: testProfile, + }, + ]; + await expect(client.patchExtendedProfile(testProfile)).resolves.toEqual(patchedProfile); + }); + + it("can replace our extended profile", async () => { + const testProfile = { + test_key: "foo", + }; + httpLookups = [ + { + method: "PUT", + prefix: unstableMSC4133Prefix, + path: "/profile/" + encodeURIComponent(client.credentials.userId!), + data: testProfile, + expectBody: testProfile, + }, + ]; + await expect(client.setExtendedProfile(testProfile)).resolves.toEqual(undefined); + }); + }); + it("should create (unstable) file trees", async () => { const userId = "@test:example.org"; const roomId = "!room:example.org"; diff --git a/src/client.ts b/src/client.ts index 04c10ceae52..ca506caa042 100644 --- a/src/client.ts +++ b/src/client.ts @@ -544,6 +544,8 @@ export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_m export const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140"; +export const UNSTABLE_MSC4133_EXTENDED_PROFILES = "uk.tcpip.msc4133"; + enum CrossSigningKeyType { MasterKey = "master_key", SelfSigningKey = "self_signing_key", @@ -8806,6 +8808,183 @@ export class MatrixClient extends TypedEventEmitter { + return this.doesServerSupportUnstableFeature(UNSTABLE_MSC4133_EXTENDED_PROFILES); + } + + /** + * Get the prefix used for extended profile requests. + * + * @returns The prefix for use with `authedRequest` + */ + private async getExtendedProfileRequestPrefix(): Promise { + if (await this.doesServerSupportUnstableFeature("uk.tcpip.msc4133.stable")) { + return ClientPrefix.V3; + } + return "/_matrix/client/unstable/uk.tcpip.msc4133"; + } + + /** + * Fetch a user's *extended* profile, which may include additonal keys. + * + * @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md + * @param userId The user ID to fetch the profile of. + * @returns A set of keys to property values. + * + * @throws An error if the server does not support MSC4133. + * @throws A M_NOT_FOUND error if the profile could not be found. + */ + public async getExtendedProfile(userId: string): Promise> { + if (!(await this.doesServerSupportExtendedProfiles())) { + throw new Error("Server does not support extended profiles"); + } + return this.http.authedRequest( + Method.Get, + utils.encodeUri("/profile/$userId", { $userId: userId }), + undefined, + undefined, + { + prefix: await this.getExtendedProfileRequestPrefix(), + }, + ); + } + + /** + * Fetch a specific key from the user's *extended* profile. + * + * @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md + * @param userId The user ID to fetch the profile of. + * @param key The key of the property to fetch. + * @returns The property value. + * + * @throws An error if the server does not support MSC4133. + * @throws A M_NOT_FOUND error if the key was not set OR the profile could not be found. + */ + public async getExtendedProfileProperty(userId: string, key: string): Promise { + if (!(await this.doesServerSupportExtendedProfiles())) { + throw new Error("Server does not support extended profiles"); + } + const profile = (await this.http.authedRequest( + Method.Get, + utils.encodeUri("/profile/$userId/$key", { $userId: userId, $key: key }), + undefined, + undefined, + { + prefix: await this.getExtendedProfileRequestPrefix(), + }, + )) as Record; + return profile[key]; + } + + /** + * Set a property on your *extended* profile. + * + * @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md + * @param key The key of the property to set. + * @param value The value to set on the propety. + * + * @throws An error if the server does not support MSC4133 OR the server disallows editing the user profile. + */ + public async setExtendedProfileProperty(key: string, value: unknown): Promise { + if (!(await this.doesServerSupportExtendedProfiles())) { + throw new Error("Server does not support extended profiles"); + } + const userId = this.getUserId(); + + await this.http.authedRequest( + Method.Put, + utils.encodeUri("/profile/$userId/$key", { $userId: userId, $key: key }), + undefined, + { [key]: value }, + { + prefix: await this.getExtendedProfileRequestPrefix(), + }, + ); + } + + /** + * Delete a property on your *extended* profile. + * + * @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md + * @param key The key of the property to delete. + * + * @throws An error if the server does not support MSC4133 OR the server disallows editing the user profile. + */ + public async deleteExtendedProfileProperty(key: string): Promise { + if (!(await this.doesServerSupportExtendedProfiles())) { + throw new Error("Server does not support extended profiles"); + } + const userId = this.getUserId(); + + await this.http.authedRequest( + Method.Delete, + utils.encodeUri("/profile/$userId/$key", { $userId: userId, $key: key }), + undefined, + undefined, + { + prefix: await this.getExtendedProfileRequestPrefix(), + }, + ); + } + + /** + * Update multiple properties on your *extended* profile. This will + * merge with any existing keys. + * + * @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md + * @param profile The profile object to merge with the existing profile. + * @returns The newly merged profile. + * + * @throws An error if the server does not support MSC4133 OR the server disallows editing the user profile. + */ + public async patchExtendedProfile(profile: Record): Promise> { + if (!(await this.doesServerSupportExtendedProfiles())) { + throw new Error("Server does not support extended profiles"); + } + const userId = this.getUserId(); + + return this.http.authedRequest( + Method.Patch, + utils.encodeUri("/profile/$userId", { $userId: userId }), + {}, + profile, + { + prefix: await this.getExtendedProfileRequestPrefix(), + }, + ); + } + + /** + * Set multiple properties on your *extended* profile. This will completely + * replace the existing profile, removing any unspecified keys. + * + * @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md + * @param profile The profile object to set. + * + * @throws An error if the server does not support MSC4133 OR the server disallows editing the user profile. + */ + public async setExtendedProfile(profile: Record): Promise { + if (!(await this.doesServerSupportExtendedProfiles())) { + throw new Error("Server does not support extended profiles"); + } + const userId = this.getUserId(); + + await this.http.authedRequest( + Method.Put, + utils.encodeUri("/profile/$userId", { $userId: userId }), + {}, + profile, + { + prefix: await this.getExtendedProfileRequestPrefix(), + }, + ); + } + /** * @returns Promise which resolves to a list of the user's threepids. * @returns Rejects: with an error response. diff --git a/src/models/profile-keys.ts b/src/models/profile-keys.ts new file mode 100644 index 00000000000..d496653cb97 --- /dev/null +++ b/src/models/profile-keys.ts @@ -0,0 +1,7 @@ +/** + * The timezone the user is currently in. The value of this property should + * match a timezone provided in https://www.iana.org/time-zones. + * + * @see https://github.com/matrix-org/matrix-spec-proposals/blob/clokep/profile-tz/proposals/4175-profile-field-time-zone.md + */ +export const ProfileKeyMSC4175Timezone = "us.cloke.msc4175.tz"; diff --git a/src/serverCapabilities.ts b/src/serverCapabilities.ts index c23c9c72e92..f477ce399cd 100644 --- a/src/serverCapabilities.ts +++ b/src/serverCapabilities.ts @@ -38,6 +38,8 @@ export interface ISetDisplayNameCapability extends ICapability {} export interface ISetAvatarUrlCapability extends ICapability {} +export interface IProfileFieldsCapability extends ICapability {} + export enum RoomVersionStability { Stable = "stable", Unstable = "unstable", @@ -61,6 +63,7 @@ export interface Capabilities { "org.matrix.msc3882.get_login_token"?: IGetLoginTokenCapability; "m.set_displayname"?: ISetDisplayNameCapability; "m.set_avatar_url"?: ISetAvatarUrlCapability; + "uk.tcpip.msc4133.profile_fields"?: IProfileFieldsCapability; } type CapabilitiesResponse = {