You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +03:00
Fetch capabilities in the background (#4246)
* Fetch capabilities in the background & keep them up to date * Add missed await * Replace some more runAllTimers and round down the wait time for sanity * Remove double comment * Typo * Add a method back that will fetch capabilities if they're not already there * Add tests * Catch exception here too * Add test for room version code
This commit is contained in:
@@ -796,7 +796,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
|||||||
|
|
||||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
jest.runAllTimers();
|
jest.advanceTimersByTime(10 * 60 * 1000);
|
||||||
await failurePromise;
|
await failurePromise;
|
||||||
|
|
||||||
// Fix the endpoint to do successful uploads
|
// Fix the endpoint to do successful uploads
|
||||||
@@ -829,7 +829,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
|||||||
});
|
});
|
||||||
|
|
||||||
// run the timers, which will make the backup loop redo the request
|
// run the timers, which will make the backup loop redo the request
|
||||||
await jest.runAllTimersAsync();
|
await jest.advanceTimersByTimeAsync(10 * 60 * 1000);
|
||||||
await successPromise;
|
await successPromise;
|
||||||
await allKeysUploadedPromise;
|
await allKeysUploadedPromise;
|
||||||
});
|
});
|
||||||
|
@@ -1293,18 +1293,109 @@ describe("MatrixClient", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getCapabilities", () => {
|
describe("getCapabilities", () => {
|
||||||
it("should cache by default", async () => {
|
it("should return cached capabilities if present", async () => {
|
||||||
|
const capsObject = {
|
||||||
|
"m.change_password": false,
|
||||||
|
};
|
||||||
|
|
||||||
|
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||||
|
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||||
|
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||||
httpBackend.when("GET", "/capabilities").respond(200, {
|
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||||
capabilities: {
|
capabilities: capsObject,
|
||||||
"m.change_password": false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const prom = httpBackend.flushAllExpected();
|
|
||||||
const capabilities1 = await client.getCapabilities();
|
client.startClient();
|
||||||
const capabilities2 = await client.getCapabilities();
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
|
expect(await client.getCapabilities()).toEqual(capsObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch capabilities if cache not present", async () => {
|
||||||
|
const capsObject = {
|
||||||
|
"m.change_password": false,
|
||||||
|
};
|
||||||
|
|
||||||
|
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||||
|
capabilities: capsObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
const capsPromise = client.getCapabilities();
|
||||||
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
|
expect(await capsPromise).toEqual(capsObject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getCachedCapabilities", () => {
|
||||||
|
it("should return cached capabilities or undefined", async () => {
|
||||||
|
const capsObject = {
|
||||||
|
"m.change_password": false,
|
||||||
|
};
|
||||||
|
|
||||||
|
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||||
|
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||||
|
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||||
|
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||||
|
capabilities: capsObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(client.getCachedCapabilities()).toBeUndefined();
|
||||||
|
|
||||||
|
client.startClient();
|
||||||
|
|
||||||
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
|
expect(client.getCachedCapabilities()).toEqual(capsObject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetchCapabilities", () => {
|
||||||
|
const capsObject = {
|
||||||
|
"m.change_password": false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||||
|
capabilities: capsObject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should always fetch capabilities and then cache", async () => {
|
||||||
|
const prom = client.fetchCapabilities();
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
|
const caps = await prom;
|
||||||
|
|
||||||
|
expect(caps).toEqual(capsObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should write-through the cache", async () => {
|
||||||
|
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||||
|
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||||
|
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||||
|
|
||||||
|
client.startClient();
|
||||||
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
|
expect(client.getCachedCapabilities()).toEqual(capsObject);
|
||||||
|
|
||||||
|
const newCapsObject = {
|
||||||
|
"m.change_password": true,
|
||||||
|
};
|
||||||
|
|
||||||
|
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||||
|
capabilities: newCapsObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prom = client.fetchCapabilities();
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
await prom;
|
await prom;
|
||||||
|
|
||||||
expect(capabilities1).toStrictEqual(capabilities2);
|
expect(client.getCachedCapabilities()).toEqual(newCapsObject);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -105,13 +105,13 @@ describe("MatrixClient syncing errors", () => {
|
|||||||
|
|
||||||
await client!.startClient();
|
await client!.startClient();
|
||||||
expect(await syncEvents[0].promise).toBe(SyncState.Error);
|
expect(await syncEvents[0].promise).toBe(SyncState.Error);
|
||||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||||
expect(await syncEvents[1].promise).toBe(SyncState.Error);
|
expect(await syncEvents[1].promise).toBe(SyncState.Error);
|
||||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||||
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
|
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
|
||||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||||
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
|
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
|
||||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||||
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
|
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,6 +119,7 @@ describe("MatrixClient syncing errors", () => {
|
|||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
fetchMock.config.overwriteRoutes = false;
|
fetchMock.config.overwriteRoutes = false;
|
||||||
fetchMock
|
fetchMock
|
||||||
|
.get("end:capabilities", {})
|
||||||
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
||||||
.get("end:versions", unknownTokenErrorData) // further version checks fails with 401
|
.get("end:versions", unknownTokenErrorData) // further version checks fails with 401
|
||||||
.get("end:pushrules/", 401) // fails with 401 without an error. This does happen in practice e.g. with Synapse
|
.get("end:pushrules/", 401) // fails with 401 without an error. This does happen in practice e.g. with Synapse
|
||||||
|
@@ -88,6 +88,6 @@ export const mockClientMethodsEvents = () => ({
|
|||||||
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
|
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
|
||||||
getIdentityServerUrl: jest.fn(),
|
getIdentityServerUrl: jest.fn(),
|
||||||
getHomeserverUrl: jest.fn(),
|
getHomeserverUrl: jest.fn(),
|
||||||
getCapabilities: jest.fn().mockReturnValue({}),
|
getCachedCapabilities: jest.fn().mockReturnValue({}),
|
||||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||||
});
|
});
|
||||||
|
@@ -116,7 +116,7 @@ function makeMockClient(opts: {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getCapabilities() {
|
getCachedCapabilities() {
|
||||||
return opts.msc3882r0Only
|
return opts.msc3882r0Only
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
|
@@ -4034,4 +4034,47 @@ describe("Room", function () {
|
|||||||
expect(room.getLastThread()).toBe(thread2);
|
expect(room.getLastThread()).toBe(thread2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getRecommendedVersion", () => {
|
||||||
|
it("returns the server's recommended version from capabilities", async () => {
|
||||||
|
const client = new TestClient(userA).client;
|
||||||
|
client.getCapabilities = jest.fn().mockReturnValue({
|
||||||
|
["m.room_versions"]: {
|
||||||
|
default: "1",
|
||||||
|
available: ["1", "2"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const room = new Room(roomId, client, userA);
|
||||||
|
expect(await room.getRecommendedVersion()).toEqual({
|
||||||
|
version: "1",
|
||||||
|
needsUpgrade: false,
|
||||||
|
urgent: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("force-refreshes versions to make sure an upgrade is necessary", async () => {
|
||||||
|
const client = new TestClient(userA).client;
|
||||||
|
client.getCapabilities = jest.fn().mockReturnValue({
|
||||||
|
["m.room_versions"]: {
|
||||||
|
default: "5",
|
||||||
|
available: ["5"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.fetchCapabilities = jest.fn().mockResolvedValue({
|
||||||
|
["m.room_versions"]: {
|
||||||
|
default: "1",
|
||||||
|
available: ["1"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const room = new Room(roomId, client, userA);
|
||||||
|
expect(await room.getRecommendedVersion()).toEqual({
|
||||||
|
version: "1",
|
||||||
|
needsUpgrade: false,
|
||||||
|
urgent: false,
|
||||||
|
});
|
||||||
|
expect(client.fetchCapabilities).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
110
src/client.ts
110
src/client.ts
@@ -226,6 +226,7 @@ import { getRelationsThreadFilter } from "./thread-utils";
|
|||||||
import { KnownMembership, Membership } from "./@types/membership";
|
import { KnownMembership, Membership } from "./@types/membership";
|
||||||
import { RoomMessageEventContent, StickerEventContent } from "./@types/events";
|
import { RoomMessageEventContent, StickerEventContent } from "./@types/events";
|
||||||
import { ImageInfo } from "./@types/media";
|
import { ImageInfo } from "./@types/media";
|
||||||
|
import { Capabilities, ServerCapabilities } from "./serverCapabilities";
|
||||||
|
|
||||||
export type Store = IStore;
|
export type Store = IStore;
|
||||||
|
|
||||||
@@ -233,7 +234,6 @@ export type ResetTimelineCallback = (roomId: string) => boolean;
|
|||||||
|
|
||||||
const SCROLLBACK_DELAY_MS = 3000;
|
const SCROLLBACK_DELAY_MS = 3000;
|
||||||
export const CRYPTO_ENABLED: boolean = isCryptoAvailable();
|
export const CRYPTO_ENABLED: boolean = isCryptoAvailable();
|
||||||
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
|
|
||||||
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
|
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
|
||||||
|
|
||||||
export const UNSTABLE_MSC3852_LAST_SEEN_UA = new UnstableValue(
|
export const UNSTABLE_MSC3852_LAST_SEEN_UA = new UnstableValue(
|
||||||
@@ -518,26 +518,6 @@ export interface IStartClientOpts {
|
|||||||
|
|
||||||
export interface IStoredClientOpts extends IStartClientOpts {}
|
export interface IStoredClientOpts extends IStartClientOpts {}
|
||||||
|
|
||||||
export enum RoomVersionStability {
|
|
||||||
Stable = "stable",
|
|
||||||
Unstable = "unstable",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRoomVersionsCapability {
|
|
||||||
default: string;
|
|
||||||
available: Record<string, RoomVersionStability>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICapability {
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IChangePasswordCapability extends ICapability {}
|
|
||||||
|
|
||||||
export interface IThreadsCapability extends ICapability {}
|
|
||||||
|
|
||||||
export interface IGetLoginTokenCapability extends ICapability {}
|
|
||||||
|
|
||||||
export const GET_LOGIN_TOKEN_CAPABILITY = new NamespacedValue(
|
export const GET_LOGIN_TOKEN_CAPABILITY = new NamespacedValue(
|
||||||
"m.get_login_token",
|
"m.get_login_token",
|
||||||
"org.matrix.msc3882.get_login_token",
|
"org.matrix.msc3882.get_login_token",
|
||||||
@@ -547,19 +527,6 @@ export const UNSTABLE_MSC2666_SHARED_ROOMS = "uk.half-shot.msc2666";
|
|||||||
export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms";
|
export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms";
|
||||||
export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_mutual_rooms";
|
export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_mutual_rooms";
|
||||||
|
|
||||||
/**
|
|
||||||
* A representation of the capabilities advertised by a homeserver as defined by
|
|
||||||
* [Capabilities negotiation](https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv3capabilities).
|
|
||||||
*/
|
|
||||||
export interface Capabilities {
|
|
||||||
[key: string]: any;
|
|
||||||
"m.change_password"?: IChangePasswordCapability;
|
|
||||||
"m.room_versions"?: IRoomVersionsCapability;
|
|
||||||
"io.element.thread"?: IThreadsCapability;
|
|
||||||
"m.get_login_token"?: IGetLoginTokenCapability;
|
|
||||||
"org.matrix.msc3882.get_login_token"?: IGetLoginTokenCapability;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CrossSigningKeyType {
|
enum CrossSigningKeyType {
|
||||||
MasterKey = "master_key",
|
MasterKey = "master_key",
|
||||||
SelfSigningKey = "self_signing_key",
|
SelfSigningKey = "self_signing_key",
|
||||||
@@ -1293,10 +1260,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
// TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020
|
// TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020
|
||||||
protected serverVersionsPromise?: Promise<IServerVersions>;
|
protected serverVersionsPromise?: Promise<IServerVersions>;
|
||||||
|
|
||||||
public cachedCapabilities?: {
|
|
||||||
capabilities: Capabilities;
|
|
||||||
expiration: number;
|
|
||||||
};
|
|
||||||
protected clientWellKnown?: IClientWellKnown;
|
protected clientWellKnown?: IClientWellKnown;
|
||||||
protected clientWellKnownPromise?: Promise<IClientWellKnown>;
|
protected clientWellKnownPromise?: Promise<IClientWellKnown>;
|
||||||
protected turnServers: ITurnServer[] = [];
|
protected turnServers: ITurnServer[] = [];
|
||||||
@@ -1325,6 +1288,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
|
|
||||||
public readonly matrixRTC: MatrixRTCSessionManager;
|
public readonly matrixRTC: MatrixRTCSessionManager;
|
||||||
|
|
||||||
|
private serverCapabilitiesService: ServerCapabilities;
|
||||||
|
|
||||||
public constructor(opts: IMatrixClientCreateOpts) {
|
public constructor(opts: IMatrixClientCreateOpts) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -1418,6 +1383,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
// the underlying session management and doesn't use any actual media capabilities
|
// the underlying session management and doesn't use any actual media capabilities
|
||||||
this.matrixRTC = new MatrixRTCSessionManager(this);
|
this.matrixRTC = new MatrixRTCSessionManager(this);
|
||||||
|
|
||||||
|
this.serverCapabilitiesService = new ServerCapabilities(this.http);
|
||||||
|
|
||||||
this.on(ClientEvent.Sync, this.fixupRoomNotifications);
|
this.on(ClientEvent.Sync, this.fixupRoomNotifications);
|
||||||
|
|
||||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||||
@@ -1540,6 +1507,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.toDeviceMessageQueue.start();
|
this.toDeviceMessageQueue.start();
|
||||||
|
this.serverCapabilitiesService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1593,6 +1561,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
this.toDeviceMessageQueue.stop();
|
this.toDeviceMessageQueue.stop();
|
||||||
|
|
||||||
this.matrixRTC.stop();
|
this.matrixRTC.stop();
|
||||||
|
|
||||||
|
this.serverCapabilitiesService.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2095,47 +2065,35 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the capabilities of the homeserver. Always returns an object of
|
* Gets the cached capabilities of the homeserver, returning cached ones if available.
|
||||||
* capability keys and their options, which may be empty.
|
* If there are no cached capabilities and none can be fetched, throw an exception.
|
||||||
* @param fresh - True to ignore any cached values.
|
*
|
||||||
* @returns Promise which resolves to the capabilities of the homeserver
|
* @returns Promise resolving with The capabilities of the homeserver
|
||||||
* @returns Rejects: with an error response.
|
|
||||||
*/
|
*/
|
||||||
public getCapabilities(fresh = false): Promise<Capabilities> {
|
public async getCapabilities(): Promise<Capabilities> {
|
||||||
const now = new Date().getTime();
|
const caps = this.serverCapabilitiesService.getCachedCapabilities();
|
||||||
|
if (caps) return caps;
|
||||||
|
return this.serverCapabilitiesService.fetchCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.cachedCapabilities && !fresh) {
|
/**
|
||||||
if (now < this.cachedCapabilities.expiration) {
|
* Gets the cached capabilities of the homeserver. If none have been fetched yet,
|
||||||
this.logger.debug("Returning cached capabilities");
|
* return undefined.
|
||||||
return Promise.resolve(this.cachedCapabilities.capabilities);
|
*
|
||||||
}
|
* @returns The capabilities of the homeserver
|
||||||
}
|
*/
|
||||||
|
public getCachedCapabilities(): Capabilities | undefined {
|
||||||
|
return this.serverCapabilitiesService.getCachedCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
type Response = {
|
/**
|
||||||
capabilities?: Capabilities;
|
* Fetches the latest capabilities from the homeserver, ignoring any cached
|
||||||
};
|
* versions. The newly returned version is cached.
|
||||||
return this.http
|
*
|
||||||
.authedRequest<Response>(Method.Get, "/capabilities")
|
* @returns A promise which resolves to the capabilities of the homeserver
|
||||||
.catch((e: Error): Response => {
|
*/
|
||||||
// We swallow errors because we need a default object anyhow
|
public fetchCapabilities(): Promise<Capabilities> {
|
||||||
this.logger.error(e);
|
return this.serverCapabilitiesService.fetchCapabilities();
|
||||||
return {};
|
|
||||||
})
|
|
||||||
.then((r = {}) => {
|
|
||||||
const capabilities = r["capabilities"] || {};
|
|
||||||
|
|
||||||
// If the capabilities missed the cache, cache it for a shorter amount
|
|
||||||
// of time to try and refresh them later.
|
|
||||||
const cacheMs = Object.keys(capabilities).length ? CAPABILITIES_CACHE_MS : 60000 + Math.random() * 5000;
|
|
||||||
|
|
||||||
this.cachedCapabilities = {
|
|
||||||
capabilities,
|
|
||||||
expiration: now + cacheMs,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.logger.debug("Caching capabilities: ", capabilities);
|
|
||||||
return capabilities;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -24,6 +24,7 @@ import { RoomWidgetClient, ICapabilities } from "./embedded";
|
|||||||
import { CryptoStore } from "./crypto/store/base";
|
import { CryptoStore } from "./crypto/store/base";
|
||||||
|
|
||||||
export * from "./client";
|
export * from "./client";
|
||||||
|
export * from "./serverCapabilities";
|
||||||
export * from "./embedded";
|
export * from "./embedded";
|
||||||
export * from "./http-api";
|
export * from "./http-api";
|
||||||
export * from "./autodiscovery";
|
export * from "./autodiscovery";
|
||||||
|
@@ -41,7 +41,7 @@ import {
|
|||||||
RelationType,
|
RelationType,
|
||||||
UNSIGNED_THREAD_ID_FIELD,
|
UNSIGNED_THREAD_ID_FIELD,
|
||||||
} from "../@types/event";
|
} from "../@types/event";
|
||||||
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
import { MatrixClient, PendingEventOrdering } from "../client";
|
||||||
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
|
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
|
||||||
import { Filter, IFilterDefinition } from "../filter";
|
import { Filter, IFilterDefinition } from "../filter";
|
||||||
import { RoomState, RoomStateEvent, RoomStateEventHandlerMap } from "./room-state";
|
import { RoomState, RoomStateEvent, RoomStateEventHandlerMap } from "./room-state";
|
||||||
@@ -70,6 +70,7 @@ import { RoomReceipts } from "./room-receipts";
|
|||||||
import { compareEventOrdering } from "./compare-event-ordering";
|
import { compareEventOrdering } from "./compare-event-ordering";
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import { KnownMembership, Membership } from "../@types/membership";
|
import { KnownMembership, Membership } from "../@types/membership";
|
||||||
|
import { Capabilities, IRoomVersionsCapability, RoomVersionStability } from "../serverCapabilities";
|
||||||
|
|
||||||
// These constants are used as sane defaults when the homeserver doesn't support
|
// These constants are used as sane defaults when the homeserver doesn't support
|
||||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||||
@@ -611,7 +612,10 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
* Resolves to the version the room should be upgraded to.
|
* Resolves to the version the room should be upgraded to.
|
||||||
*/
|
*/
|
||||||
public async getRecommendedVersion(): Promise<IRecommendedVersion> {
|
public async getRecommendedVersion(): Promise<IRecommendedVersion> {
|
||||||
const capabilities = await this.client.getCapabilities();
|
let capabilities: Capabilities = {};
|
||||||
|
try {
|
||||||
|
capabilities = await this.client.getCapabilities();
|
||||||
|
} catch (e) {}
|
||||||
let versionCap = capabilities["m.room_versions"];
|
let versionCap = capabilities["m.room_versions"];
|
||||||
if (!versionCap) {
|
if (!versionCap) {
|
||||||
versionCap = {
|
versionCap = {
|
||||||
@@ -636,8 +640,12 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
"to be supporting a newer room version we don't know about.",
|
"to be supporting a newer room version we don't know about.",
|
||||||
);
|
);
|
||||||
|
|
||||||
const caps = await this.client.getCapabilities(true);
|
try {
|
||||||
versionCap = caps["m.room_versions"];
|
capabilities = await this.client.fetchCapabilities();
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("Failed to refresh room version capabilities", e);
|
||||||
|
}
|
||||||
|
versionCap = capabilities["m.room_versions"];
|
||||||
if (!versionCap) {
|
if (!versionCap) {
|
||||||
logger.warn("No room version capability - assuming upgrade required.");
|
logger.warn("No room version capability - assuming upgrade required.");
|
||||||
return result;
|
return result;
|
||||||
|
@@ -22,12 +22,12 @@ import {
|
|||||||
LegacyRendezvousFailureReason as RendezvousFailureReason,
|
LegacyRendezvousFailureReason as RendezvousFailureReason,
|
||||||
RendezvousIntent,
|
RendezvousIntent,
|
||||||
} from ".";
|
} from ".";
|
||||||
import { IGetLoginTokenCapability, MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "../client";
|
import { MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "../client";
|
||||||
import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature";
|
import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { sleep } from "../utils";
|
import { sleep } from "../utils";
|
||||||
import { CrossSigningKey } from "../crypto-api";
|
import { CrossSigningKey } from "../crypto-api";
|
||||||
import { Device } from "../matrix";
|
import { Capabilities, Device, IGetLoginTokenCapability } from "../matrix";
|
||||||
|
|
||||||
enum PayloadType {
|
enum PayloadType {
|
||||||
Start = "m.login.start",
|
Start = "m.login.start",
|
||||||
@@ -109,7 +109,10 @@ export class MSC3906Rendezvous {
|
|||||||
logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`);
|
logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`);
|
||||||
|
|
||||||
// in stable and unstable r1 the availability is exposed as a capability
|
// in stable and unstable r1 the availability is exposed as a capability
|
||||||
const capabilities = await this.client.getCapabilities();
|
let capabilities: Capabilities = {};
|
||||||
|
try {
|
||||||
|
capabilities = await this.client.getCapabilities();
|
||||||
|
} catch (e) {}
|
||||||
// in r0 of MSC3882 the availability is exposed as a feature flag
|
// in r0 of MSC3882 the availability is exposed as a feature flag
|
||||||
const features = await buildFeatureSupportMap(await this.client.getVersions());
|
const features = await buildFeatureSupportMap(await this.client.getVersions());
|
||||||
const capability = GET_LOGIN_TOKEN_CAPABILITY.findIn<IGetLoginTokenCapability>(capabilities);
|
const capability = GET_LOGIN_TOKEN_CAPABILITY.findIn<IGetLoginTokenCapability>(capabilities);
|
||||||
|
136
src/serverCapabilities.ts
Normal file
136
src/serverCapabilities.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 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 { IHttpOpts, MatrixHttpApi, Method } from "./http-api";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
|
||||||
|
// How often we update the server capabilities.
|
||||||
|
// 6 hours - an arbitrary value, but they should change very infrequently.
|
||||||
|
const CAPABILITIES_CACHE_MS = 6 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
// How long we want before retrying if we couldn't fetch
|
||||||
|
const CAPABILITIES_RETRY_MS = 30 * 1000;
|
||||||
|
|
||||||
|
export interface ICapability {
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChangePasswordCapability extends ICapability {}
|
||||||
|
|
||||||
|
export interface IThreadsCapability extends ICapability {}
|
||||||
|
|
||||||
|
export interface IGetLoginTokenCapability extends ICapability {}
|
||||||
|
|
||||||
|
export interface ISetDisplayNameCapability extends ICapability {}
|
||||||
|
|
||||||
|
export interface ISetAvatarUrlCapability extends ICapability {}
|
||||||
|
|
||||||
|
export enum RoomVersionStability {
|
||||||
|
Stable = "stable",
|
||||||
|
Unstable = "unstable",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRoomVersionsCapability {
|
||||||
|
default: string;
|
||||||
|
available: Record<string, RoomVersionStability>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of the capabilities advertised by a homeserver as defined by
|
||||||
|
* [Capabilities negotiation](https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv3capabilities).
|
||||||
|
*/
|
||||||
|
export interface Capabilities {
|
||||||
|
[key: string]: any;
|
||||||
|
"m.change_password"?: IChangePasswordCapability;
|
||||||
|
"m.room_versions"?: IRoomVersionsCapability;
|
||||||
|
"io.element.thread"?: IThreadsCapability;
|
||||||
|
"m.get_login_token"?: IGetLoginTokenCapability;
|
||||||
|
"org.matrix.msc3882.get_login_token"?: IGetLoginTokenCapability;
|
||||||
|
"m.set_displayname"?: ISetDisplayNameCapability;
|
||||||
|
"m.set_avatar_url"?: ISetAvatarUrlCapability;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CapabilitiesResponse = {
|
||||||
|
capabilities: Capabilities;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages storing and periodically refreshing the server capabilities.
|
||||||
|
*/
|
||||||
|
export class ServerCapabilities {
|
||||||
|
private capabilities?: Capabilities;
|
||||||
|
private retryTimeout?: ReturnType<typeof setTimeout>;
|
||||||
|
private refreshTimeout?: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
public constructor(private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts periodically fetching the server capabilities.
|
||||||
|
*/
|
||||||
|
public start(): void {
|
||||||
|
this.poll().then();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service
|
||||||
|
*/
|
||||||
|
public stop(): void {
|
||||||
|
this.clearTimeouts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the cached capabilities, or undefined if none are cached.
|
||||||
|
* @returns the current capabilities, if any.
|
||||||
|
*/
|
||||||
|
public getCachedCapabilities(): Capabilities | undefined {
|
||||||
|
return this.capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the latest server capabilities from the homeserver and returns them, or rejects
|
||||||
|
* on failure.
|
||||||
|
*/
|
||||||
|
public fetchCapabilities = async (): Promise<Capabilities> => {
|
||||||
|
const resp = await this.http.authedRequest<CapabilitiesResponse>(Method.Get, "/capabilities");
|
||||||
|
this.capabilities = resp["capabilities"];
|
||||||
|
return this.capabilities;
|
||||||
|
};
|
||||||
|
|
||||||
|
private poll = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await this.fetchCapabilities();
|
||||||
|
this.clearTimeouts();
|
||||||
|
this.refreshTimeout = setTimeout(this.poll, CAPABILITIES_CACHE_MS);
|
||||||
|
logger.debug("Fetched new server capabilities");
|
||||||
|
} catch (e) {
|
||||||
|
this.clearTimeouts();
|
||||||
|
const howLong = Math.floor(CAPABILITIES_RETRY_MS + Math.random() * 5000);
|
||||||
|
this.retryTimeout = setTimeout(this.poll, howLong);
|
||||||
|
logger.warn(`Failed to refresh capabilities: retrying in ${howLong}ms`, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private clearTimeouts(): void {
|
||||||
|
if (this.refreshTimeout) {
|
||||||
|
clearInterval(this.refreshTimeout);
|
||||||
|
this.refreshTimeout = undefined;
|
||||||
|
}
|
||||||
|
if (this.retryTimeout) {
|
||||||
|
clearTimeout(this.retryTimeout);
|
||||||
|
this.retryTimeout = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user