From 337d0726ce3cb495b4ec7b3bfa4b02e0d6a71272 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 6 Dec 2021 11:36:45 +0530 Subject: [PATCH 01/11] WIP --- src/matrix/net/HomeServerApi.js | 273 +++++++++++++++++++++++++++ src/matrix/room/timeline/Timeline.js | 2 +- 2 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 src/matrix/net/HomeServerApi.js diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js new file mode 100644 index 00000000..a5756e33 --- /dev/null +++ b/src/matrix/net/HomeServerApi.js @@ -0,0 +1,273 @@ +/* +Copyright 2020 Bruno Windels +Copyright 2020 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 {encodeQueryParams, encodeBody} from "./common.js"; +import {HomeServerRequest} from "./HomeServerRequest.js"; + +const CS_R0_PREFIX = "/_matrix/client/r0"; +const DEHYDRATION_PREFIX = "/_matrix/client/unstable/org.matrix.msc2697.v2"; + +export class HomeServerApi { + constructor({homeserver, accessToken, request, reconnector}) { + // store these both in a closure somehow so it's harder to get at in case of XSS? + // one could change the homeserver as well so the token gets sent there, so both must be protected from read/write + this._homeserver = homeserver; + this._accessToken = accessToken; + this._requestFn = request; + this._reconnector = reconnector; + } + + _url(csPath, prefix = CS_R0_PREFIX) { + return this._homeserver + prefix + csPath; + } + + _baseRequest(method, url, queryParams, body, options, accessToken) { + const queryString = encodeQueryParams(queryParams); + url = `${url}?${queryString}`; + let log; + if (options?.log) { + const parent = options?.log; + log = parent.child({ + t: "network", + url, + method, + }, parent.level.Info); + } + let encodedBody; + const headers = new Map(); + if (accessToken) { + headers.set("Authorization", `Bearer ${accessToken}`); + } + headers.set("Accept", "application/json"); + if (body) { + const encoded = encodeBody(body); + headers.set("Content-Type", encoded.mimeType); + headers.set("Content-Length", encoded.length); + encodedBody = encoded.body; + } + + const requestResult = this._requestFn(url, { + method, + headers, + body: encodedBody, + timeout: options?.timeout, + uploadProgress: options?.uploadProgress, + format: "json" // response format + }); + + const hsRequest = new HomeServerRequest(method, url, requestResult, log); + + if (this._reconnector) { + hsRequest.response().catch(err => { + // Some endpoints such as /sync legitimately time-out + // (which is also reported as a ConnectionError) and will re-attempt, + // but spinning up the reconnector in this case is ok, + // as all code ran on session and sync start should be reentrant + if (err.name === "ConnectionError") { + this._reconnector.onRequestFailed(this); + } + }); + } + + return hsRequest; + } + + _unauthedRequest(method, url, queryParams, body, options) { + return this._baseRequest(method, url, queryParams, body, options, null); + } + + _authedRequest(method, url, queryParams, body, options) { + return this._baseRequest(method, url, queryParams, body, options, this._accessToken); + } + + _post(csPath, queryParams, body, options) { + return this._authedRequest("POST", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); + } + + _put(csPath, queryParams, body, options) { + return this._authedRequest("PUT", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); + } + + _get(csPath, queryParams, body, options) { + return this._authedRequest("GET", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); + } + + sync(since, filter, timeout, options = null) { + return this._get("/sync", {since, timeout, filter}, null, options); + } + + event(roomId, eventId) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(eventId)}`); + } + + // params is from, dir and optionally to, limit, filter. + messages(roomId, params, options = null) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, null, options); + } + + // params is at, membership and not_membership + members(roomId, params, options = null) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, null, options); + } + + send(roomId, eventType, txnId, content, options = null) { + return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options); + } + + redact(roomId, eventId, txnId, content, options = null) { + return this._put(`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`, {}, content, options); + } + + receipt(roomId, receiptType, eventId, options = null) { + return this._post(`/rooms/${encodeURIComponent(roomId)}/receipt/${encodeURIComponent(receiptType)}/${encodeURIComponent(eventId)}`, + {}, {}, options); + } + + state(roomId, eventType, stateKey, options = null) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options); + } + + getLoginFlows() { + return this._unauthedRequest("GET", this._url("/login"), null, null, null); + } + + passwordLogin(username, password, initialDeviceDisplayName, options = null) { + return this._unauthedRequest("POST", this._url("/login"), null, { + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": username + }, + "password": password, + "initial_device_display_name": initialDeviceDisplayName + }, options); + } + + tokenLogin(loginToken, txnId, initialDeviceDisplayName, options = null) { + return this._unauthedRequest("POST", this._url("/login"), null, { + "type": "m.login.token", + "identifier": { + "type": "m.id.user", + }, + "token": loginToken, + "txn_id": txnId, + "initial_device_display_name": initialDeviceDisplayName + }, options); + } + + createFilter(userId, filter, options = null) { + return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options); + } + + versions(options = null) { + return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options); + } + + uploadKeys(dehydratedDeviceId, payload, options = null) { + let path = "/keys/upload"; + if (dehydratedDeviceId) { + path = path + `/${encodeURIComponent(dehydratedDeviceId)}`; + } + return this._post(path, null, payload, options); + } + + queryKeys(queryRequest, options = null) { + return this._post("/keys/query", null, queryRequest, options); + } + + claimKeys(payload, options = null) { + return this._post("/keys/claim", null, payload, options); + } + + sendToDevice(type, payload, txnId, options = null) { + return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, null, payload, options); + } + + roomKeysVersion(version = null, options = null) { + let versionPart = ""; + if (version) { + versionPart = `/${encodeURIComponent(version)}`; + } + return this._get(`/room_keys/version${versionPart}`, null, null, options); + } + + roomKeyForRoomAndSession(version, roomId, sessionId, options = null) { + return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, null, options); + } + + uploadAttachment(blob, filename, options = null) { + return this._authedRequest("POST", `${this._homeserver}/_matrix/media/r0/upload`, {filename}, blob, options); + } + + setPusher(pusher, options = null) { + return this._post("/pushers/set", null, pusher, options); + } + + getPushers(options = null) { + return this._get("/pushers", null, null, options); + } + + join(roomId, options = null) { + return this._post(`/rooms/${encodeURIComponent(roomId)}/join`, null, null, options); + } + + joinIdOrAlias(roomIdOrAlias, options = null) { + return this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`, null, null, options); + } + + leave(roomId, options = null) { + return this._post(`/rooms/${encodeURIComponent(roomId)}/leave`, null, null, options); + } + + forget(roomId, options = null) { + return this._post(`/rooms/${encodeURIComponent(roomId)}/forget`, null, null, options); + } + + logout(options = null) { + return this._post(`/logout`, null, null, options); + } + + getDehydratedDevice(options = {}) { + options.prefix = DEHYDRATION_PREFIX; + return this._get(`/dehydrated_device`, null, null, options); + } + + createDehydratedDevice(payload, options = {}) { + options.prefix = DEHYDRATION_PREFIX; + return this._put(`/dehydrated_device`, null, payload, options); + } + + claimDehydratedDevice(deviceId, options = {}) { + options.prefix = DEHYDRATION_PREFIX; + return this._post(`/dehydrated_device/claim`, null, {device_id: deviceId}, options); + } +} + +import {Request as MockRequest} from "../../mocks/Request.js"; + +export function tests() { + return { + "superficial happy path for GET": async assert => { + const hsApi = new HomeServerApi({ + request: () => new MockRequest().respond(200, 42), + homeserver: "https://hs.tld" + }); + const result = await hsApi._get("foo", null, null, null).response(); + assert.strictEqual(result, 42); + } + } +} diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index ef3c3fba..e018fa24 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -224,7 +224,7 @@ export class Timeline { } /** @package */ - replaceEntries(entries) { + replaceEntries(entries) { this._addLocalRelationsToNewRemoteEntries(entries); for (const entry of entries) { try { From 9eeeea47d9dd9f7e2b4f5d80adab6dce951801b6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Dec 2021 17:55:04 +0530 Subject: [PATCH 02/11] Treat replies to thread as threaded message --- src/matrix/room/timeline/entries/BaseEventEntry.js | 6 +++++- src/matrix/room/timeline/entries/reply.js | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/entries/BaseEventEntry.js b/src/matrix/room/timeline/entries/BaseEventEntry.js index ec20f48a..b3ac3e9c 100644 --- a/src/matrix/room/timeline/entries/BaseEventEntry.js +++ b/src/matrix/room/timeline/entries/BaseEventEntry.js @@ -32,7 +32,11 @@ export class BaseEventEntry extends BaseEntry { } get isReply() { - return !!this.relation?.["m.in_reply_to"]; + return !!(this.relation?.["m.in_reply_to"] || this.isThread); + } + + get isThread() { + return this.relation?.["rel_type"] === "io.element.thread"; } get isRedacting() { diff --git a/src/matrix/room/timeline/entries/reply.js b/src/matrix/room/timeline/entries/reply.js index 2e180c11..bbb2d53d 100644 --- a/src/matrix/room/timeline/entries/reply.js +++ b/src/matrix/room/timeline/entries/reply.js @@ -51,6 +51,9 @@ function _createReplyContent(targetId, msgtype, body, formattedBody) { } export function createReplyContent(entry, msgtype, body) { + if (entry.isThread) { + return createThreadContent(entry, msgtype, body); + } // TODO check for absense of sender / body / msgtype / etc? const nonTextual = fallbackForNonTextualMessage(entry.content.msgtype); const prefix = fallbackPrefix(entry.content.msgtype); @@ -72,3 +75,14 @@ export function createReplyContent(entry, msgtype, body) { const newFormattedBody = formattedFallback + htmlEscape(body); return _createReplyContent(entry.id, msgtype, newBody, newFormattedBody); } + +function createThreadContent(entry, msgtype, body) { + return { + msgtype, + body, + "m.relates_to": { + "rel_type": "m.thread", + "event_id": entry.id + } + }; +} From 78c79b148a4f3a0370b5f0419bc0f2cf46c780cb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 20 Dec 2021 13:08:44 +0530 Subject: [PATCH 03/11] Refactor code --- src/matrix/room/timeline/entries/BaseEventEntry.js | 4 ++-- src/matrix/room/timeline/entries/reply.js | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/matrix/room/timeline/entries/BaseEventEntry.js b/src/matrix/room/timeline/entries/BaseEventEntry.js index b3ac3e9c..884bffa9 100644 --- a/src/matrix/room/timeline/entries/BaseEventEntry.js +++ b/src/matrix/room/timeline/entries/BaseEventEntry.js @@ -18,7 +18,7 @@ import {BaseEntry} from "./BaseEntry"; import {REDACTION_TYPE} from "../../common.js"; import {createAnnotation, ANNOTATION_RELATION_TYPE, getRelationFromContent} from "../relations.js"; import {PendingAnnotation} from "../PendingAnnotation.js"; -import {createReplyContent} from "./reply.js" +import {createReplyContent, THREADING_REL_TYPE} from "./reply.js" /** Deals mainly with local echo for relations and redactions, * so it is shared between PendingEventEntry and EventEntry */ @@ -36,7 +36,7 @@ export class BaseEventEntry extends BaseEntry { } get isThread() { - return this.relation?.["rel_type"] === "io.element.thread"; + return this.relation?.["rel_type"] === THREADING_REL_TYPE; } get isRedacting() { diff --git a/src/matrix/room/timeline/entries/reply.js b/src/matrix/room/timeline/entries/reply.js index bbb2d53d..2fbbd0d4 100644 --- a/src/matrix/room/timeline/entries/reply.js +++ b/src/matrix/room/timeline/entries/reply.js @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {getRelatedEventId} from "../relations.js"; + +export const THREADING_REL_TYPE = "io.element.thread"; + function htmlEscape(string) { return string.replace(/&/g, "&").replace(//g, ">"); } @@ -81,8 +85,8 @@ function createThreadContent(entry, msgtype, body) { msgtype, body, "m.relates_to": { - "rel_type": "m.thread", - "event_id": entry.id + "rel_type": THREADING_REL_TYPE, + "event_id": getRelatedEventId(entry) } }; } From 825bec80b08068d1f11e964a9a8c9fbd42570dcc Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 20 Dec 2021 13:34:22 +0530 Subject: [PATCH 04/11] Prefer reply over thread when both are available --- src/matrix/room/timeline/relations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/relations.js b/src/matrix/room/timeline/relations.js index 4009d8c4..2d9217ad 100644 --- a/src/matrix/room/timeline/relations.js +++ b/src/matrix/room/timeline/relations.js @@ -30,7 +30,7 @@ export function createAnnotation(targetId, key) { } export function getRelationTarget(relation) { - return relation.event_id || relation["m.in_reply_to"]?.event_id + return relation["m.in_reply_to"]?.event_id || relation.event_id; } export function setRelationTarget(relation, target) { From 38781db8582fed73d11e9c6fef576dc706ed8ce1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 20 Dec 2021 16:42:30 +0530 Subject: [PATCH 05/11] Implement quote replies to thread --- src/matrix/room/timeline/entries/reply.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/matrix/room/timeline/entries/reply.js b/src/matrix/room/timeline/entries/reply.js index 2fbbd0d4..8b503118 100644 --- a/src/matrix/room/timeline/entries/reply.js +++ b/src/matrix/room/timeline/entries/reply.js @@ -40,8 +40,8 @@ function fallbackPrefix(msgtype) { return msgtype === "m.emote" ? "* " : ""; } -function _createReplyContent(targetId, msgtype, body, formattedBody) { - return { +function _createReplyContent(targetId, msgtype, body, formattedBody, threadId) { + const reply = { msgtype, body, "format": "org.matrix.custom.html", @@ -52,10 +52,18 @@ function _createReplyContent(targetId, msgtype, body, formattedBody) { } } }; + if (threadId) { + Object.assign(reply["m.relates_to"], { + rel_type: THREADING_REL_TYPE, + event_id: threadId, + }); + } + return reply; } export function createReplyContent(entry, msgtype, body) { - if (entry.isThread) { + // don't use entry.isReply here since we pretend that threads are replies + if (!entry.relation["m.in_reply_to"] && entry.isThread) { return createThreadContent(entry, msgtype, body); } // TODO check for absense of sender / body / msgtype / etc? @@ -77,7 +85,7 @@ export function createReplyContent(entry, msgtype, body) { const newBody = plainFallback + '\n\n' + body; const newFormattedBody = formattedFallback + htmlEscape(body); - return _createReplyContent(entry.id, msgtype, newBody, newFormattedBody); + return _createReplyContent(entry.id, msgtype, newBody, newFormattedBody, entry.threadEventId); } function createThreadContent(entry, msgtype, body) { From f44fa775dec66e73201b8240b2726e9d122941f5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 22 Dec 2021 12:37:36 +0530 Subject: [PATCH 06/11] Move threading relation type const to relations.js --- src/matrix/room/timeline/entries/BaseEventEntry.js | 6 +++--- src/matrix/room/timeline/entries/reply.js | 8 +++----- src/matrix/room/timeline/relations.js | 1 + 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/matrix/room/timeline/entries/BaseEventEntry.js b/src/matrix/room/timeline/entries/BaseEventEntry.js index 884bffa9..dca486f6 100644 --- a/src/matrix/room/timeline/entries/BaseEventEntry.js +++ b/src/matrix/room/timeline/entries/BaseEventEntry.js @@ -16,9 +16,9 @@ limitations under the License. import {BaseEntry} from "./BaseEntry"; import {REDACTION_TYPE} from "../../common.js"; -import {createAnnotation, ANNOTATION_RELATION_TYPE, getRelationFromContent} from "../relations.js"; +import {createAnnotation, ANNOTATION_RELATION_TYPE, THREADING_RELATION_TYPE, getRelationFromContent} from "../relations.js"; import {PendingAnnotation} from "../PendingAnnotation.js"; -import {createReplyContent, THREADING_REL_TYPE} from "./reply.js" +import {createReplyContent} from "./reply.js"; /** Deals mainly with local echo for relations and redactions, * so it is shared between PendingEventEntry and EventEntry */ @@ -36,7 +36,7 @@ export class BaseEventEntry extends BaseEntry { } get isThread() { - return this.relation?.["rel_type"] === THREADING_REL_TYPE; + return this.relation?.["rel_type"] === THREADING_RELATION_TYPE; } get isRedacting() { diff --git a/src/matrix/room/timeline/entries/reply.js b/src/matrix/room/timeline/entries/reply.js index 8b503118..7b05b272 100644 --- a/src/matrix/room/timeline/entries/reply.js +++ b/src/matrix/room/timeline/entries/reply.js @@ -14,9 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {getRelatedEventId} from "../relations.js"; - -export const THREADING_REL_TYPE = "io.element.thread"; +import {getRelatedEventId, THREADING_RELATION_TYPE} from "../relations.js"; function htmlEscape(string) { return string.replace(/&/g, "&").replace(//g, ">"); @@ -54,7 +52,7 @@ function _createReplyContent(targetId, msgtype, body, formattedBody, threadId) { }; if (threadId) { Object.assign(reply["m.relates_to"], { - rel_type: THREADING_REL_TYPE, + rel_type: THREADING_RELATION_TYPE, event_id: threadId, }); } @@ -93,7 +91,7 @@ function createThreadContent(entry, msgtype, body) { msgtype, body, "m.relates_to": { - "rel_type": THREADING_REL_TYPE, + "rel_type": THREADING_RELATION_TYPE, "event_id": getRelatedEventId(entry) } }; diff --git a/src/matrix/room/timeline/relations.js b/src/matrix/room/timeline/relations.js index 2d9217ad..d92847ec 100644 --- a/src/matrix/room/timeline/relations.js +++ b/src/matrix/room/timeline/relations.js @@ -18,6 +18,7 @@ import {REDACTION_TYPE} from "../common.js"; export const REACTION_TYPE = "m.reaction"; export const ANNOTATION_RELATION_TYPE = "m.annotation"; +export const THREADING_RELATION_TYPE = "io.element.thread"; export function createAnnotation(targetId, key) { return { From 6a556d73adb414428f60439abce7808b2b9c8269 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 22 Dec 2021 15:16:33 +0530 Subject: [PATCH 07/11] Always include reply content --- src/matrix/room/timeline/entries/reply.js | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/matrix/room/timeline/entries/reply.js b/src/matrix/room/timeline/entries/reply.js index 7b05b272..fcc6f56f 100644 --- a/src/matrix/room/timeline/entries/reply.js +++ b/src/matrix/room/timeline/entries/reply.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {getRelatedEventId, THREADING_RELATION_TYPE} from "../relations.js"; +import {THREADING_RELATION_TYPE} from "../relations.js"; function htmlEscape(string) { return string.replace(/&/g, "&").replace(//g, ">"); @@ -60,11 +60,6 @@ function _createReplyContent(targetId, msgtype, body, formattedBody, threadId) { } export function createReplyContent(entry, msgtype, body) { - // don't use entry.isReply here since we pretend that threads are replies - if (!entry.relation["m.in_reply_to"] && entry.isThread) { - return createThreadContent(entry, msgtype, body); - } - // TODO check for absense of sender / body / msgtype / etc? const nonTextual = fallbackForNonTextualMessage(entry.content.msgtype); const prefix = fallbackPrefix(entry.content.msgtype); const sender = entry.sender; @@ -85,14 +80,3 @@ export function createReplyContent(entry, msgtype, body) { const newFormattedBody = formattedFallback + htmlEscape(body); return _createReplyContent(entry.id, msgtype, newBody, newFormattedBody, entry.threadEventId); } - -function createThreadContent(entry, msgtype, body) { - return { - msgtype, - body, - "m.relates_to": { - "rel_type": THREADING_RELATION_TYPE, - "event_id": getRelatedEventId(entry) - } - }; -} From 72691eeb9d77ba60771d53665e6ab0ed30be6816 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 6 Jan 2022 17:02:07 +0530 Subject: [PATCH 08/11] Delete ghost .js file --- src/matrix/net/HomeServerApi.js | 273 -------------------------------- 1 file changed, 273 deletions(-) delete mode 100644 src/matrix/net/HomeServerApi.js diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js deleted file mode 100644 index a5756e33..00000000 --- a/src/matrix/net/HomeServerApi.js +++ /dev/null @@ -1,273 +0,0 @@ -/* -Copyright 2020 Bruno Windels -Copyright 2020 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 {encodeQueryParams, encodeBody} from "./common.js"; -import {HomeServerRequest} from "./HomeServerRequest.js"; - -const CS_R0_PREFIX = "/_matrix/client/r0"; -const DEHYDRATION_PREFIX = "/_matrix/client/unstable/org.matrix.msc2697.v2"; - -export class HomeServerApi { - constructor({homeserver, accessToken, request, reconnector}) { - // store these both in a closure somehow so it's harder to get at in case of XSS? - // one could change the homeserver as well so the token gets sent there, so both must be protected from read/write - this._homeserver = homeserver; - this._accessToken = accessToken; - this._requestFn = request; - this._reconnector = reconnector; - } - - _url(csPath, prefix = CS_R0_PREFIX) { - return this._homeserver + prefix + csPath; - } - - _baseRequest(method, url, queryParams, body, options, accessToken) { - const queryString = encodeQueryParams(queryParams); - url = `${url}?${queryString}`; - let log; - if (options?.log) { - const parent = options?.log; - log = parent.child({ - t: "network", - url, - method, - }, parent.level.Info); - } - let encodedBody; - const headers = new Map(); - if (accessToken) { - headers.set("Authorization", `Bearer ${accessToken}`); - } - headers.set("Accept", "application/json"); - if (body) { - const encoded = encodeBody(body); - headers.set("Content-Type", encoded.mimeType); - headers.set("Content-Length", encoded.length); - encodedBody = encoded.body; - } - - const requestResult = this._requestFn(url, { - method, - headers, - body: encodedBody, - timeout: options?.timeout, - uploadProgress: options?.uploadProgress, - format: "json" // response format - }); - - const hsRequest = new HomeServerRequest(method, url, requestResult, log); - - if (this._reconnector) { - hsRequest.response().catch(err => { - // Some endpoints such as /sync legitimately time-out - // (which is also reported as a ConnectionError) and will re-attempt, - // but spinning up the reconnector in this case is ok, - // as all code ran on session and sync start should be reentrant - if (err.name === "ConnectionError") { - this._reconnector.onRequestFailed(this); - } - }); - } - - return hsRequest; - } - - _unauthedRequest(method, url, queryParams, body, options) { - return this._baseRequest(method, url, queryParams, body, options, null); - } - - _authedRequest(method, url, queryParams, body, options) { - return this._baseRequest(method, url, queryParams, body, options, this._accessToken); - } - - _post(csPath, queryParams, body, options) { - return this._authedRequest("POST", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); - } - - _put(csPath, queryParams, body, options) { - return this._authedRequest("PUT", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); - } - - _get(csPath, queryParams, body, options) { - return this._authedRequest("GET", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); - } - - sync(since, filter, timeout, options = null) { - return this._get("/sync", {since, timeout, filter}, null, options); - } - - event(roomId, eventId) { - return this._get(`/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(eventId)}`); - } - - // params is from, dir and optionally to, limit, filter. - messages(roomId, params, options = null) { - return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, null, options); - } - - // params is at, membership and not_membership - members(roomId, params, options = null) { - return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, null, options); - } - - send(roomId, eventType, txnId, content, options = null) { - return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options); - } - - redact(roomId, eventId, txnId, content, options = null) { - return this._put(`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`, {}, content, options); - } - - receipt(roomId, receiptType, eventId, options = null) { - return this._post(`/rooms/${encodeURIComponent(roomId)}/receipt/${encodeURIComponent(receiptType)}/${encodeURIComponent(eventId)}`, - {}, {}, options); - } - - state(roomId, eventType, stateKey, options = null) { - return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options); - } - - getLoginFlows() { - return this._unauthedRequest("GET", this._url("/login"), null, null, null); - } - - passwordLogin(username, password, initialDeviceDisplayName, options = null) { - return this._unauthedRequest("POST", this._url("/login"), null, { - "type": "m.login.password", - "identifier": { - "type": "m.id.user", - "user": username - }, - "password": password, - "initial_device_display_name": initialDeviceDisplayName - }, options); - } - - tokenLogin(loginToken, txnId, initialDeviceDisplayName, options = null) { - return this._unauthedRequest("POST", this._url("/login"), null, { - "type": "m.login.token", - "identifier": { - "type": "m.id.user", - }, - "token": loginToken, - "txn_id": txnId, - "initial_device_display_name": initialDeviceDisplayName - }, options); - } - - createFilter(userId, filter, options = null) { - return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options); - } - - versions(options = null) { - return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options); - } - - uploadKeys(dehydratedDeviceId, payload, options = null) { - let path = "/keys/upload"; - if (dehydratedDeviceId) { - path = path + `/${encodeURIComponent(dehydratedDeviceId)}`; - } - return this._post(path, null, payload, options); - } - - queryKeys(queryRequest, options = null) { - return this._post("/keys/query", null, queryRequest, options); - } - - claimKeys(payload, options = null) { - return this._post("/keys/claim", null, payload, options); - } - - sendToDevice(type, payload, txnId, options = null) { - return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, null, payload, options); - } - - roomKeysVersion(version = null, options = null) { - let versionPart = ""; - if (version) { - versionPart = `/${encodeURIComponent(version)}`; - } - return this._get(`/room_keys/version${versionPart}`, null, null, options); - } - - roomKeyForRoomAndSession(version, roomId, sessionId, options = null) { - return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, null, options); - } - - uploadAttachment(blob, filename, options = null) { - return this._authedRequest("POST", `${this._homeserver}/_matrix/media/r0/upload`, {filename}, blob, options); - } - - setPusher(pusher, options = null) { - return this._post("/pushers/set", null, pusher, options); - } - - getPushers(options = null) { - return this._get("/pushers", null, null, options); - } - - join(roomId, options = null) { - return this._post(`/rooms/${encodeURIComponent(roomId)}/join`, null, null, options); - } - - joinIdOrAlias(roomIdOrAlias, options = null) { - return this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`, null, null, options); - } - - leave(roomId, options = null) { - return this._post(`/rooms/${encodeURIComponent(roomId)}/leave`, null, null, options); - } - - forget(roomId, options = null) { - return this._post(`/rooms/${encodeURIComponent(roomId)}/forget`, null, null, options); - } - - logout(options = null) { - return this._post(`/logout`, null, null, options); - } - - getDehydratedDevice(options = {}) { - options.prefix = DEHYDRATION_PREFIX; - return this._get(`/dehydrated_device`, null, null, options); - } - - createDehydratedDevice(payload, options = {}) { - options.prefix = DEHYDRATION_PREFIX; - return this._put(`/dehydrated_device`, null, payload, options); - } - - claimDehydratedDevice(deviceId, options = {}) { - options.prefix = DEHYDRATION_PREFIX; - return this._post(`/dehydrated_device/claim`, null, {device_id: deviceId}, options); - } -} - -import {Request as MockRequest} from "../../mocks/Request.js"; - -export function tests() { - return { - "superficial happy path for GET": async assert => { - const hsApi = new HomeServerApi({ - request: () => new MockRequest().respond(200, 42), - homeserver: "https://hs.tld" - }); - const result = await hsApi._get("foo", null, null, null).response(); - assert.strictEqual(result, 42); - } - } -} From 22356bee519b5b4733ece5c60f05b2a725d67156 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 13 Jan 2022 21:01:13 +0530 Subject: [PATCH 09/11] No need to git add . whole expression --- src/matrix/room/timeline/entries/BaseEventEntry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/entries/BaseEventEntry.js b/src/matrix/room/timeline/entries/BaseEventEntry.js index dca486f6..dbcfce94 100644 --- a/src/matrix/room/timeline/entries/BaseEventEntry.js +++ b/src/matrix/room/timeline/entries/BaseEventEntry.js @@ -32,7 +32,7 @@ export class BaseEventEntry extends BaseEntry { } get isReply() { - return !!(this.relation?.["m.in_reply_to"] || this.isThread); + return !!this.relation?.["m.in_reply_to"] || this.isThread; } get isThread() { From c3784d406b77f07eff9a0807de78265bdb318579 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 13 Jan 2022 21:05:57 +0530 Subject: [PATCH 10/11] Move getter to this PR --- src/matrix/room/timeline/entries/EventEntry.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 7b957e01..3d9c982b 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -124,6 +124,13 @@ export class EventEntry extends BaseEventEntry { return getRelatedEventId(this.event); } + get threadEventId() { + if (this.isThread) { + return this.relation?.event_id; + } + return null; + } + get isRedacted() { return super.isRedacted || isRedacted(this._eventEntry.event); } From 1572934195af8e29869115a0aa922497f1393102 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Jan 2022 19:12:05 +0100 Subject: [PATCH 11/11] remove accidental space --- src/matrix/room/timeline/Timeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index e018fa24..ef3c3fba 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -224,7 +224,7 @@ export class Timeline { } /** @package */ - replaceEntries(entries) { + replaceEntries(entries) { this._addLocalRelationsToNewRemoteEntries(entries); for (const entry of entries) { try {