Skip to content

Commit

Permalink
Implement MSC4039: Add an MSC for a new Widget API action to upload f…
Browse files Browse the repository at this point in the history
…iles into the media repository (#86)

* Add an action to upload media files according to MSC4039

Signed-off-by: Dominik Henneke <[email protected]>

* Extract interface in the WidgetDriver

Signed-off-by: Dominik Henneke <[email protected]>

---------

Signed-off-by: Dominik Henneke <[email protected]>
  • Loading branch information
dhenneke authored Aug 28, 2023
1 parent c2d9d0b commit 79a1496
Show file tree
Hide file tree
Showing 13 changed files with 537 additions and 0 deletions.
57 changes: 57 additions & 0 deletions src/ClientWidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ import {
IReadRoomAccountDataFromWidgetActionRequest,
IReadRoomAccountDataFromWidgetResponseData,
} from "./interfaces/ReadRoomAccountDataAction";
import {
IGetMediaConfigActionFromWidgetActionRequest,
IGetMediaConfigActionFromWidgetResponseData,
} from "./interfaces/GetMediaConfigAction";
import {
IUploadFileActionFromWidgetActionRequest,
IUploadFileActionFromWidgetResponseData,
} from "./interfaces/UploadFileAction";

/**
* API handler for the client side of widgets. This raises events
Expand Down Expand Up @@ -703,6 +711,50 @@ export class ClientWidgetApi extends EventEmitter {
}
}

private async handleGetMediaConfig(request: IGetMediaConfigActionFromWidgetActionRequest) {
if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Missing capability" },
});
}

try {
const result = await this.driver.getMediaConfig()

return this.transport.reply<IGetMediaConfigActionFromWidgetResponseData>(
request,
result,
);
} catch (e) {
console.error("error while getting the media configuration", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Unexpected error while getting the media configuration" },
});
}
}

private async handleUploadFile(request: IUploadFileActionFromWidgetActionRequest) {
if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Missing capability" },
});
}

try {
const result = await this.driver.uploadFile(request.data.file);

return this.transport.reply<IUploadFileActionFromWidgetResponseData>(
request,
{ content_uri: result.contentUri },
);
} catch (e) {
console.error("error while uploading a file", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Unexpected error while uploading a file" },
});
}
}

private handleMessage(ev: CustomEvent<IWidgetApiRequest>) {
if (this.isStopped) return;
const actionEv = new CustomEvent(`action:${ev.detail.action}`, {
Expand Down Expand Up @@ -738,6 +790,11 @@ export class ClientWidgetApi extends EventEmitter {
return this.handleUserDirectorySearch(<IUserDirectorySearchFromWidgetActionRequest>ev.detail)
case WidgetApiFromWidgetAction.BeeperReadRoomAccountData:
return this.handleReadRoomAccountData(<IReadRoomAccountDataFromWidgetActionRequest>ev.detail);
case WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction:
return this.handleGetMediaConfig(<IGetMediaConfigActionFromWidgetActionRequest>ev.detail);
case WidgetApiFromWidgetAction.MSC4039UploadFileAction:
return this.handleUploadFile(<IUploadFileActionFromWidgetActionRequest>ev.detail);

default:
return this.transport.reply(ev.detail, <IWidgetApiErrorResponseData>{
error: {
Expand Down
48 changes: 48 additions & 0 deletions src/WidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ import {
IUserDirectorySearchFromWidgetRequestData,
IUserDirectorySearchFromWidgetResponseData,
} from "./interfaces/UserDirectorySearchAction";
import {
IGetMediaConfigActionFromWidgetRequestData,
IGetMediaConfigActionFromWidgetResponseData,
} from "./interfaces/GetMediaConfigAction";
import {
IUploadFileActionFromWidgetRequestData,
IUploadFileActionFromWidgetResponseData,
} from "./interfaces/UploadFileAction";

/**
* API handler for widgets. This raises events for each action
Expand Down Expand Up @@ -662,6 +670,46 @@ export class WidgetApi extends EventEmitter {
>(WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data);
}

/**
* Get the config for the media repository.
* @returns Promise which resolves with an object containing the config.
*/
public async getMediaConfig(): Promise<IGetMediaConfigActionFromWidgetResponseData> {
const versions = await this.getClientVersions();
if (!versions.includes(UnstableApiVersion.MSC4039)) {
throw new Error("The get_media_config action is not supported by the client.")
}

const data: IGetMediaConfigActionFromWidgetRequestData = {};

return this.transport.send<
IGetMediaConfigActionFromWidgetRequestData,
IGetMediaConfigActionFromWidgetResponseData
>(WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data);
}

/**
* Upload a file to the media repository on the homeserver.
* @param file - The object to upload. Something that can be sent to
* XMLHttpRequest.send (typically a File).
* @returns Resolves to the location of the uploaded file.
*/
public async uploadFile(file: XMLHttpRequestBodyInit): Promise<IUploadFileActionFromWidgetResponseData> {
const versions = await this.getClientVersions();
if (!versions.includes(UnstableApiVersion.MSC4039)) {
throw new Error("The upload_file action is not supported by the client.")
}

const data: IUploadFileActionFromWidgetRequestData = {
file,
};

return this.transport.send<
IUploadFileActionFromWidgetRequestData,
IUploadFileActionFromWidgetResponseData
>(WidgetApiFromWidgetAction.MSC4039UploadFileAction, data);
}

/**
* 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
25 changes: 25 additions & 0 deletions src/driver/WidgetDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export interface ISearchUserDirectoryResult {
}>;
}

export interface IGetMediaConfigResult {
[key: string]: unknown;
"m.upload.size"?: number;
}

/**
* Represents the functions and behaviour the widget-api is unable to
* do, such as prompting the user for information or interacting with
Expand Down Expand Up @@ -274,4 +279,24 @@ export abstract class WidgetDriver {
): Promise<ISearchUserDirectoryResult> {
return Promise.resolve({ limited: false, results: [] });
}

/**
* Get the config for the media repository.
* @returns Promise which resolves with an object containing the config.
*/
public getMediaConfig(): Promise<IGetMediaConfigResult> {
throw new Error("Get media config is not implemented");
}

/**
* Upload a file to the media repository on the homeserver.
* @param file - The object to upload. Something that can be sent to
* XMLHttpRequest.send (typically a File).
* @returns Resolves to the location of the uploaded file.
*/
public uploadFile(
file: XMLHttpRequestBodyInit,
): Promise<{ contentUri: string }> {
throw new Error("Upload file is not implemented");
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export * from "./interfaces/IRoomAccountData";
export * from "./interfaces/NavigateAction";
export * from "./interfaces/TurnServerActions";
export * from "./interfaces/ReadRelationsAction";
export * from "./interfaces/GetMediaConfigAction";
export * from "./interfaces/UploadFileAction";

// 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 @@ -30,6 +30,7 @@ export enum UnstableApiVersion {
MSC3846 = "town.robin.msc3846",
MSC3869 = "org.matrix.msc3869",
MSC3973 = "org.matrix.msc3973",
MSC4039 = "org.matrix.msc4039",
}

export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string;
Expand All @@ -47,4 +48,5 @@ export const CurrentApiVersions: ApiVersion[] = [
UnstableApiVersion.MSC3846,
UnstableApiVersion.MSC3869,
UnstableApiVersion.MSC3973,
UnstableApiVersion.MSC4039,
];
4 changes: 4 additions & 0 deletions src/interfaces/Capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export enum MatrixCapabilities {
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
*/
MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search",
/**
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
*/
MSC4039UploadFile = "org.matrix.msc4039.upload_file",
}

export type Capability = MatrixCapabilities | string;
Expand Down
38 changes: 38 additions & 0 deletions src/interfaces/GetMediaConfigAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2023 Nordeck IT + Consulting GmbH.
*
* 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 } from "./IWidgetApiRequest";
import { IWidgetApiResponseData } from "./IWidgetApiResponse";
import { WidgetApiFromWidgetAction } from "./WidgetApiAction";

export interface IGetMediaConfigActionFromWidgetRequestData
extends IWidgetApiRequestData {}

export interface IGetMediaConfigActionFromWidgetActionRequest
extends IWidgetApiRequest {
action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction;
data: IGetMediaConfigActionFromWidgetRequestData;
}

export interface IGetMediaConfigActionFromWidgetResponseData
extends IWidgetApiResponseData {
"m.upload.size"?: number;
}

export interface IGetMediaConfigActionFromWidgetActionResponse
extends IGetMediaConfigActionFromWidgetActionRequest {
response: IGetMediaConfigActionFromWidgetResponseData;
}
40 changes: 40 additions & 0 deletions src/interfaces/UploadFileAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2023 Nordeck IT + Consulting GmbH.
*
* 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 } from "./IWidgetApiRequest";
import { IWidgetApiResponseData } from "./IWidgetApiResponse";
import { WidgetApiFromWidgetAction } from "./WidgetApiAction";

export interface IUploadFileActionFromWidgetRequestData
extends IWidgetApiRequestData {
file: XMLHttpRequestBodyInit;
}

export interface IUploadFileActionFromWidgetActionRequest
extends IWidgetApiRequest {
action: WidgetApiFromWidgetAction.MSC4039UploadFileAction;
data: IUploadFileActionFromWidgetRequestData;
}

export interface IUploadFileActionFromWidgetResponseData
extends IWidgetApiResponseData {
content_uri: string; // eslint-disable-line camelcase
}

export interface IUploadFileActionFromWidgetActionResponse
extends IUploadFileActionFromWidgetActionRequest {
response: IUploadFileActionFromWidgetResponseData;
}
10 changes: 10 additions & 0 deletions src/interfaces/WidgetApiAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ export enum WidgetApiFromWidgetAction {
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
*/
MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search",

/**
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
*/
MSC4039GetMediaConfigAction = "org.matrix.msc4039.get_media_config",

/**
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
*/
MSC4039UploadFileAction = "org.matrix.msc4039.upload_file",
}

export type WidgetApiAction = WidgetApiToWidgetAction | WidgetApiFromWidgetAction | string;
4 changes: 4 additions & 0 deletions src/templating/url-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface ITemplateParams {
clientTheme?: string;
clientLanguage?: string;
deviceId?: string;
baseUrl?: string;
}

export function runTemplate(url: string, widget: IWidget, params: ITemplateParams): string {
Expand All @@ -43,6 +44,9 @@ export function runTemplate(url: string, widget: IWidget, params: ITemplateParam

// TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/3819)
'org.matrix.msc3819.matrix_device_id': params.deviceId || "",

// TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/4039)
'org.matrix.msc4039.matrix_base_url': params.baseUrl || "",
});
let result = url;
for (const key of Object.keys(variables)) {
Expand Down
Loading

0 comments on commit 79a1496

Please sign in to comment.