Skip to content

Commit

Permalink
MSC4133 - Extended profiles (#4391)
Browse files Browse the repository at this point in the history
* Add MSC4133 functionality.

* Add MSC4133 capability.

* Tidy

* Add tests for extended profiles.

* improve docs

* undefined

* Add a prefix function to reduce reptitiveness

* Add a docstring
  • Loading branch information
Half-Shot authored Sep 9, 2024
1 parent ba7bd06 commit e8128d3
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 0 deletions.
118 changes: 118 additions & 0 deletions spec/unit/matrix-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
179 changes: 179 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -8806,6 +8808,183 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest(Method.Get, path);
}

/**
* Determine if the server supports extended profiles, as described by MSC4133.
*
* @returns `true` if supported, otherwise `false`
*/
public async doesServerSupportExtendedProfiles(): Promise<boolean> {
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<string> {
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<Record<string, unknown>> {
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<unknown> {
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<string, unknown>;
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<void> {
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<void> {
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<string, unknown>): Promise<Record<string, unknown>> {
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<string, unknown>): Promise<void> {
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.
Expand Down
7 changes: 7 additions & 0 deletions src/models/profile-keys.ts
Original file line number Diff line number Diff line change
@@ -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";
3 changes: 3 additions & 0 deletions src/serverCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 = {
Expand Down

0 comments on commit e8128d3

Please sign in to comment.