Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/ClientWidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ import { SimpleObservable } from "./util/SimpleObservable";
import { IOpenIDCredentialsActionRequestData } from "./interfaces/OpenIDCredentialsAction";
import { INavigateActionRequest } from "./interfaces/NavigateAction";
import { IReadEventFromWidgetActionRequest, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction";
import {
ITurnServer,
IWatchTurnServersRequest,
IUnwatchTurnServersRequest,
IUpdateTurnServersRequestData,
} from "./interfaces/TurnServerActions";
import { Symbols } from "./Symbols";

/**
Expand Down Expand Up @@ -100,6 +106,7 @@ export class ClientWidgetApi extends EventEmitter {
private allowedCapabilities = new Set<Capability>();
private allowedEvents: WidgetEventCapability[] = [];
private isStopped = false;
private turnServers: AsyncGenerator<ITurnServer> | null = null;

/**
* Creates a new client widget API. This will instantiate the transport
Expand Down Expand Up @@ -492,6 +499,71 @@ export class ClientWidgetApi extends EventEmitter {
}
}

private async pollTurnServers(turnServers: AsyncGenerator<ITurnServer>, initialServer: ITurnServer) {
try {
await this.transport.send<IUpdateTurnServersRequestData>(
WidgetApiToWidgetAction.UpdateTurnServers,
initialServer as IUpdateTurnServersRequestData, // it's compatible, but missing the index signature
);

// Pick the generator up where we left off
for await (const server of turnServers) {
await this.transport.send<IUpdateTurnServersRequestData>(
WidgetApiToWidgetAction.UpdateTurnServers,
server as IUpdateTurnServersRequestData, // it's compatible, but missing the index signature
);
}
} catch (e) {
console.error("error polling for TURN servers", e);
}
}

private async handleWatchTurnServers(request: IWatchTurnServersRequest): Promise<void> {
if (!this.hasCapability(MatrixCapabilities.MSC3846TurnServers)) {
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Missing capability"},
});
} else if (this.turnServers) {
// We're already polling, so this is a no-op
await this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
} else {
try {
const turnServers = this.driver.getTurnServers();

// Peek at the first result, so we can at least verify that the
// client isn't banned from getting TURN servers entirely
const { done, value } = await turnServers.next();
if (done) throw new Error("Client refuses to provide any TURN servers");
await this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});

// Start the poll loop, sending the widget the initial result
this.pollTurnServers(turnServers, value);
this.turnServers = turnServers;
} catch (e) {
console.error("error getting first TURN server results", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "TURN servers not available"},
});
}
}
}

private async handleUnwatchTurnServers(request: IUnwatchTurnServersRequest): Promise<void> {
if (!this.hasCapability(MatrixCapabilities.MSC3846TurnServers)) {
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Missing capability"},
});
} else if (!this.turnServers) {
// We weren't polling anyways, so this is a no-op
await this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
} else {
// Stop the generator, allowing it to clean up
await this.turnServers.return(undefined);
this.turnServers = null;
await this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
}
}

private handleMessage(ev: CustomEvent<IWidgetApiRequest>) {
if (this.isStopped) return;
const actionEv = new CustomEvent(`action:${ev.detail.action}`, {
Expand All @@ -517,6 +589,10 @@ export class ClientWidgetApi extends EventEmitter {
return this.handleCapabilitiesRenegotiate(<IRenegotiateCapabilitiesActionRequest>ev.detail);
case WidgetApiFromWidgetAction.MSC2876ReadEvents:
return this.handleReadEvents(<IReadEventFromWidgetActionRequest>ev.detail);
case WidgetApiFromWidgetAction.WatchTurnServers:
return this.handleWatchTurnServers(<IWatchTurnServersRequest>ev.detail);
case WidgetApiFromWidgetAction.UnwatchTurnServers:
return this.handleUnwatchTurnServers(<IUnwatchTurnServersRequest>ev.detail);
default:
return this.transport.reply(ev.detail, <IWidgetApiErrorResponseData>{
error: {
Expand Down
50 changes: 50 additions & 0 deletions src/WidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { EventEmitter } from "events";
import { Capability } from "./interfaces/Capabilities";
import { IWidgetApiRequest, IWidgetApiRequestEmptyData } from "./interfaces/IWidgetApiRequest";
import { IWidgetApiAcknowledgeResponseData } from "./interfaces/IWidgetApiResponse";
import { WidgetApiDirection } from "./interfaces/WidgetApiDirection";
import {
ISupportedVersionsActionRequest,
Expand Down Expand Up @@ -61,6 +62,7 @@ import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapab
import { INavigateActionRequestData } from "./interfaces/NavigateAction";
import { IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction";
import { IRoomEvent } from "./interfaces/IRoomEvent";
import { ITurnServer, IUpdateTurnServersRequest } from "./interfaces/TurnServerActions";
import { Symbols } from "./Symbols";

/**
Expand Down Expand Up @@ -88,6 +90,7 @@ export class WidgetApi extends EventEmitter {
private requestedCapabilities: Capability[] = [];
private approvedCapabilities: Capability[];
private cachedClientVersions: ApiVersion[];
private turnServerWatchers = 0;

/**
* Creates a new API handler for the given widget.
Expand Down Expand Up @@ -487,6 +490,53 @@ export class WidgetApi extends EventEmitter {
).then();
}

/**
* Starts watching for TURN servers, yielding an initial set of credentials as soon as possible,
* and thereafter yielding new credentials whenever the previous ones expire.
* @yields {ITurnServer} The TURN server URIs and credentials currently available to the widget.
*/
public async* getTurnServers(): AsyncGenerator<ITurnServer> {
let setTurnServer: (server: ITurnServer) => void;

const onUpdateTurnServers = async (ev: CustomEvent<IUpdateTurnServersRequest>) => {
ev.preventDefault();
setTurnServer(ev.detail.data);
await this.transport.reply<IWidgetApiAcknowledgeResponseData>(ev.detail, {});
};

// Start listening for updates before we even start watching, to catch
// TURN data that is sent immediately
this.on(`action:${WidgetApiToWidgetAction.UpdateTurnServers}`, onUpdateTurnServers);

// Only send the 'watch' action if we aren't already watching
if (this.turnServerWatchers === 0) {
try {
await this.transport.send<IWidgetApiRequestEmptyData>(WidgetApiFromWidgetAction.WatchTurnServers, {});
} catch (e) {
this.off(`action:${WidgetApiToWidgetAction.UpdateTurnServers}`, onUpdateTurnServers);
throw e;
}
}
this.turnServerWatchers++;

try {
// Watch for new data indefinitely (until this generator's return method is called)
while (true) {
yield await new Promise<ITurnServer>(resolve => setTurnServer = resolve);
}
} finally {
// The loop was broken by the caller - clean up
this.off(`action:${WidgetApiToWidgetAction.UpdateTurnServers}`, onUpdateTurnServers);

// Since sending the 'unwatch' action will end updates for all other
// consumers, only send it if we're the only consumer remaining
this.turnServerWatchers--;
if (this.turnServerWatchers === 0) {
await this.transport.send<IWidgetApiRequestEmptyData>(WidgetApiFromWidgetAction.UnwatchTurnServers, {});
}
}
}

/**
* Starts the communication channel. This should be done early to ensure
* that messages are not missed. Communication can only be stopped by the client.
Expand Down
12 changes: 11 additions & 1 deletion src/driver/WidgetDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { Capability, IOpenIDCredentials, OpenIDRequestState, SimpleObservable, IRoomEvent } from "..";
import { Capability, IOpenIDCredentials, OpenIDRequestState, SimpleObservable, IRoomEvent, ITurnServer } from "..";

export interface ISendEventDetails {
roomId: string;
Expand Down Expand Up @@ -169,4 +169,14 @@ export abstract class WidgetDriver {
public navigate(uri: string): Promise<void> {
throw new Error("Navigation is not implemented");
}

/**
* Polls for TURN server data, yielding an initial set of credentials as soon as possible, and
* thereafter yielding new credentials whenever the previous ones expire. The widget API will
* have already verified that the widget has permission to access TURN servers.
* @yields {ITurnServer} The TURN server URIs and credentials currently available to the client.
*/
public getTurnServers(): AsyncGenerator<ITurnServer> {
throw new Error("TURN server support is not implemented");
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export * from "./interfaces/SendToDeviceAction";
export * from "./interfaces/ReadEventAction";
export * from "./interfaces/IRoomEvent";
export * from "./interfaces/NavigateAction";
export * from "./interfaces/TurnServerActions";

// Complex models
export * from "./models/WidgetEventCapability";
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/ApiVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export enum UnstableApiVersion {
MSC2974 = "org.matrix.msc2974",
MSC2876 = "org.matrix.msc2876",
MSC3819 = "org.matrix.msc3819",
MSC3846 = "town.robin.msc3846",
}

export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string;
Expand All @@ -41,4 +42,5 @@ export const CurrentApiVersions: ApiVersion[] = [
UnstableApiVersion.MSC2974,
UnstableApiVersion.MSC2876,
UnstableApiVersion.MSC3819,
UnstableApiVersion.MSC3846,
];
1 change: 1 addition & 0 deletions src/interfaces/Capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export enum MatrixCapabilities {
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
*/
MSC2931Navigate = "org.matrix.msc2931.navigate",
MSC3846TurnServers = "town.robin.msc3846.turn_servers",
}

export type Capability = MatrixCapabilities | string;
Expand Down
55 changes: 55 additions & 0 deletions src/interfaces/TurnServerActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 { IWidgetApiRequest, IWidgetApiRequestData, IWidgetApiRequestEmptyData } from "./IWidgetApiRequest";
import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction";
import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponse } from "./IWidgetApiResponse";

export interface ITurnServer {
uris: string[];
username: string;
password: string;
}

export interface IWatchTurnServersRequest extends IWidgetApiRequest {
action: WidgetApiFromWidgetAction.WatchTurnServers;
data: IWidgetApiRequestEmptyData;
}

export interface IWatchTurnServersResponse extends IWidgetApiResponse {
response: IWidgetApiAcknowledgeResponseData;
}

export interface IUnwatchTurnServersRequest extends IWidgetApiRequest {
action: WidgetApiFromWidgetAction.UnwatchTurnServers;
data: IWidgetApiRequestEmptyData;
}

export interface IUnwatchTurnServersResponse extends IWidgetApiResponse {
response: IWidgetApiAcknowledgeResponseData;
}

export interface IUpdateTurnServersRequestData extends IWidgetApiRequestData, ITurnServer {
}

export interface IUpdateTurnServersRequest extends IWidgetApiRequest {
action: WidgetApiToWidgetAction.UpdateTurnServers;
data: IUpdateTurnServersRequestData;
}

export interface IUpdateTurnServersResponse extends IWidgetApiResponse {
response: IWidgetApiAcknowledgeResponseData;
}
3 changes: 3 additions & 0 deletions src/interfaces/WidgetApiAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum WidgetApiToWidgetAction {
ButtonClicked = "button_clicked",
SendEvent = "send_event",
SendToDevice = "send_to_device",
UpdateTurnServers = "update_turn_servers",
}

export enum WidgetApiFromWidgetAction {
Expand All @@ -39,6 +40,8 @@ export enum WidgetApiFromWidgetAction {
SetModalButtonEnabled = "set_button_enabled",
SendEvent = "send_event",
SendToDevice = "send_to_device",
WatchTurnServers = "watch_turn_servers",
UnwatchTurnServers = "unwatch_turn_servers",

/**
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
Expand Down
6 changes: 5 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
"sourceMap": true,
"outDir": "./lib",
"declaration": true,
"types": []
"types": [],
"lib": [
"es2020",
"dom"
]
},
"include": [
"./src/**/*.ts"
Expand Down