Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement MSC4039: Add an MSC for a new Widget API action to upload files into the media repository #86

Merged
merged 3 commits into from
Aug 28, 2023
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
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