diff --git a/src/domain/rageshake.ts b/src/domain/rageshake.ts new file mode 100644 index 00000000..e79422cd --- /dev/null +++ b/src/domain/rageshake.ts @@ -0,0 +1,69 @@ +/* +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 {encodeBody} from "../matrix/net/common"; + +import type {BlobHandle} from "../platform/web/dom/BlobHandle"; +import type {RequestFunction} from "../platform/types/types"; + +// see https://github.com/matrix-org/rageshake#readme +type RageshakeData = { + // A textual description of the problem. Included in the details.log.gz file. + text: string | undefined; + // Application user-agent. Included in the details.log.gz file. + userAgent: string; + // Identifier for the application (eg 'riot-web'). Should correspond to a mapping configured in the configuration file for github issue reporting to work. + app: string; + // Application version. Included in the details.log.gz file. + version: string; + // Label to attach to the github issue, and include in the details file. + label: string | undefined; +}; + +export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: BlobHandle, submitUrl: string, request: RequestFunction): Promise { + const formData = new Map(); + if (data.text) { + formData.set("text", data.text); + } + formData.set("user_agent", data.userAgent); + formData.set("app", data.app); + formData.set("version", data.version); + if (data.label) { + formData.set("label", data.label); + } + formData.set("file", {name: "logs.json", blob: logsBlob}); + const encoded = encodeBody(formData); + const headers: Map = new Map(); + headers.set("Accept", "application/json"); + //headers.set("Content-Type", encoded.mimeType); + const result = request(submitUrl, { + method: "POST", + body: encoded.body, + headers + }); + let response; + try { + response = await result.response(); + } catch (err) { + throw new Error(`Could not submit logs to ${submitUrl}, got error ${err.message}`); + } + const {status, body} = response; + if (status >= 200 && status < 300) { + return body.report_url; + } else { + throw new Error(`Could not submit logs to ${submitUrl}, got status code ${status} with body ${body}`); + } +} diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index d844e21b..b3ca74ca 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -16,6 +16,7 @@ limitations under the License. import {ViewModel} from "../../ViewModel"; import {KeyBackupViewModel} from "./KeyBackupViewModel.js"; +import {submitLogsToRageshakeServer} from "../../../domain/rageshake"; class PushNotificationStatus { constructor() { @@ -152,6 +153,21 @@ export class SettingsViewModel extends ViewModel { this.platform.saveFileAs(logExport.asBlob(), `hydrogen-logs-${this.platform.clock.now()}.json`); } + async sendLogsToServer() { + const logExport = await this.logger.export(); + await submitLogsToRageshakeServer( + { + app: "hydrogen", + userAgent: "", + version: DEFINE_VERSION, + text: "Submit logs from settings", + }, + logExport.asBlob(), + this.platform.config.bugReportEndpointUrl, + this.platform.request + ); + } + async togglePushNotifications() { this.pushNotifications.updating = true; this.pushNotifications.enabledOnServer = null; diff --git a/src/matrix/net/common.ts b/src/matrix/net/common.ts index 4ba42395..0b35a3e3 100644 --- a/src/matrix/net/common.ts +++ b/src/matrix/net/common.ts @@ -19,7 +19,8 @@ import {BlobHandle} from "../../platform/web/dom/BlobHandle.js"; export type EncodedBody = { mimeType: string; - body: BlobHandle | string; + // the map gets transformed to a FormData object on the web + body: BlobHandle | string | Map; } export function encodeQueryParams(queryParams?: object): string { @@ -41,6 +42,11 @@ export function encodeBody(body: BlobHandle | object): EncodedBody { mimeType: blob.mimeType, body: blob // will be unwrapped in request fn }; + } else if (body instanceof Map) { + return { + mimeType: "multipart/form-data", + body: body + } } else if (typeof body === "object") { const json = JSON.stringify(body); return { diff --git a/src/platform/web/assets/config.json b/src/platform/web/assets/config.json index 703ae1e6..fd46fcbc 100644 --- a/src/platform/web/assets/config.json +++ b/src/platform/web/assets/config.json @@ -4,5 +4,6 @@ "gatewayUrl": "https://matrix.org", "applicationServerKey": "BC-gpSdVHEXhvHSHS0AzzWrQoukv2BE7KzpoPO_FfPacqOo3l1pdqz7rSgmB04pZCWaHPz7XRe6fjLaC-WPDopM" }, - "defaultHomeServer": "matrix.org" + "defaultHomeServer": "matrix.org", + "bugReportEndpointUrl": "https://element.io/bugreports/submit" } diff --git a/src/platform/web/dom/request/fetch.js b/src/platform/web/dom/request/fetch.js index adf833ef..1d2ef3c0 100644 --- a/src/platform/web/dom/request/fetch.js +++ b/src/platform/web/dom/request/fetch.js @@ -64,12 +64,26 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) { if (requestOptions?.uploadProgress) { return xhrRequest(url, requestOptions); } - let {method, headers, body, timeout, format, cache = false} = requestOptions; + let {method, headers, body, formData, timeout, format, cache = false} = requestOptions; const controller = typeof AbortController === "function" ? new AbortController() : null; // if a BlobHandle, take native blob if (body?.nativeBlob) { body = body.nativeBlob; } + if (body instanceof Map) { + const formData = new FormData(); + for (const [name, value] of body) { + let filename; + // Special case {name: string, blob: BlobHandle} to set a filename. + // This is the format returned by platform.openFile + if (value.blob?.nativeBlob && value.name) { + formData.set(name, value.blob.nativeBlob, value.name); + } else { + formData.set(name, value); + } + } + body = formData; + } let options = {method, body}; if (controller) { options = Object.assign(options, {